9/19/2009 3:58:22 PM

[設計案例] 生命遊戲 #4, 有效率的使用執行緒

Microsoft.NET | 543 | C# | Threading | 我的作品 | 技術隨筆 | 物件導向 Facebook Share

原本這篇不講執行緒,要直接跳到 OOP 多型的應用... 不過看一看 #3 自己寫的程式,實在有點看不下去... 30x30 的大小,程式跑起來就看到 903 條執行緒在那邊跑... 而看一下 CPU usage, 只有 5% 不到... 這實在不是很好看的實作範例,如果這是線上遊戲的 SERVER 程式,裡面的每個人,每個怪物等等都用一條專用的執行緒在控制他的行為的話,我看這遊戲不用太多人玩,SERVER 就掛掉了吧! 因此要繼續更貼近實際的生命模擬遊戲前,我們先來解決效能的問題,所以多安插了這篇進來 :D

前一篇 (#3) 的主題是把生命的進行,從被動的在固定時間被喚醒 (callback) 的作法,改成主動的在指定時間執行 (execute)。 想也知道,現實世界的生物都是 "主動" 的,後面的作法比較符合 OOP 的 "模擬世界,加以處理" 的精神。但是,一個小程式就吃掉 900 條執行緒,是有點過頭了。不知道還有沒有人記得,我騙到獎品的這個程式... 很另類的用 yield return 來解決類似問題的作法... 藉著 compiler 很雞婆的把單一流程翻成數段可以切開執行的邏輯...,正好拿來利用一下,替我們把一連串連續的邏輯切段,以便利用多執行緒來處理。我的想法是這樣,原程式是用個迴圈,作完該作的事,就休息 (sleep) 一段時間。而新的寫法,我打算用 yield return new TimeSpan(…) 來取代 Thread.Sleep(…)。每個 Cell內部的程式結構修改不大,不過對於 GameHost 就是個挑戰了... 來看看修改前及修改後的程式碼:

用 yield return TimeSpan 來取代 Thread.Sleep( ) 的作法[copy code]
   1:  // 修改前
   2:  // 使用 Thread.Sleep( ) 來控制時間
   3:  public void WholeLife(object state)
   4:  {
   5:      int generation = (int)state;
   6:      for (int index = 0; index < generation; index++)
   7:      {
   8:          this.OnNextStateChange();
   9:          Thread.Sleep(_rnd.Next(950, 1050));
  10:      }
  11:  }
  12:  //
  13:  //
  14:  //
  15:  // 修改後
  16:  // 使用 yield return new TimeSpan( ) 來控制時間
  17:  public IEnumerable<TimeSpan> WholeLife(object state)
  18:  {
  19:      int generation = (int)state;
  20:      for (int index = 0; index < generation; index++)
  21:      {
  22:          this.OnNextStateChange();
  23:          yield return TimeSpan.FromMilliseconds(_rnd.Next(950, 1050));
  24:      }
  25:      yield break;
  26:  }

 

別想的太美,只改這樣,程式是不會動的... 修改過之後,麻煩的地方會在 GameHost. 因為整個 GameHost 的邏輯都反過來了。原本是 GameHost 只要放著那九百條執行緒自生自滅,它只要不斷的刷新畫面就好了。現在它則得用 foreach(…) 去詢問:

"大爺,這次您要休息多久?"

接到 yield return 傳回的 TimeSpan 物件 (代表它要休息多久後,繼續下一個動作) 後,經過這段時間,GameHost 就要再去叫醒 cell, 然後再詢問一次:

"大爺,這次您要休息多久?"

關鍵就在於 GameHost 如何能透過少量的 thread 來伺後這些大爺,而不是像 #3 的程式一樣,每個大爺都用一條專屬的 thread… 要共用執行緒,就要先想辦法把工作切碎,這是基本法則。如果你希望你的生命遊戲程式不只是作業的話,那麼效能跟即時回應的問題是必需要考慮的。在動手改寫 GameHost 程式之前,先來分析一下改寫的目標有那些:

目標是要達到像 #2 範例一樣的效果,但是要用更有效率的方式。

目標很清楚,再來就看看有什麼手段可以用了。第一個是過量的執行緒,應該要想辦法改用執行緒集區。因為 #2 用了高達 900 條執行緒,不過整體 CPU USAGE 不到 5%,大部份的執行緒都在閒置狀態。如果能想辦法把這些運算丟到執行緒集區,由集區動態管理會有效率的多。

第二,就是把原本的 Thread.Sleep(ts) 改成 yield return ts 後,原本每個 thread 自己睡覺的機制,就要改成 cell 各自回報 game host 它想要睡多久,然後由 game host 統一在時間到時叫醒它。由於一次有多個 cell 同時在運作,因此我們需要一個簡單的排程器,作法像這樣:

  1. 建立一個時間表,依照時間順序,把每個 cell 預計要被叫醒的時間標上去。
  2. 時間到了之後,就去呼叫該 cell 的 OnNextStateChangeEx(),同時取得該 cell 下次要喚醒的時間,再標到時間表上
  3. GameHost就不斷的替每個 cell 重複 (1) (2) 的動作..
  4. 同時另外用一條獨立的執行緒,作畫面更新的動作。

嗯,要處理的方式越來越清楚了。剩下的是 "時間表" 要用什麼型式來表現? 我的選擇是,我希望它是個 ToDo List, 會幫我排好時間,我只要把工作標上時間,丟進 ToDo List, 然後 ToDo List 只要能忠實的回報給我還有沒有排定的工作? 如果有,下一個要處理的工作是那一個? 什麼時後處理?

它的用法只有丟工作進去,跟拿工作出來,因此我設計它的公開介面長這個樣子:

CellToDoList 的類別設計[copy code]
   1:  public class CellToDoList
   2:  {
   3:      public void AddCell(Cell cell) {...}
   4:      public Cell GetNextCell() {...}
   5:      public Cell CheckNextCell() {...}
   6:      public int Count {get;}
   7:  }

 

裡面的實作,我就不多說了。我是把它當成 QUEUE 在設計,唯一的差別是,放進 QUEUE 的東西會先經過排序,因此不見得是 "First In First Out" 這種典型的貯列,而是會以 Cell 上標示的時間為準,依序 Out …。實作起來很簡單,用現成的 SortedList 當內部的儲存方式,加上基本的 lock 機制來確保它是 thread safe 的就夠了。

好,這些雞絲都準備好之後,就可以來打造我們的新版 GameHost 了。來看看 Code:

改用 ThreadPool / CellToDoList 的新版 GameHost:[copy code]
   1:  static CellToDoList _cq;
   2:  static void _YieldReturnGameHost(string[] args)
   3:  {
   4:      int worldSizeX = 30;
   5:      int worldSizeY = 30;
   6:      World realworld = new World(worldSizeX, worldSizeY);
   7:      _cq = new CellToDoList();
   8:      // init threads for each cell
   9:      for (int positionX = 0; positionX < worldSizeX; positionX++)
  10:      {
  11:          for (int positionY = 0; positionY < worldSizeY; positionY++)
  12:          {
  13:              Cell cell = realworld.GetCell(positionX, positionY);
  14:              cell.OnNextStateChangeEx();
  15:              _cq.AddCell(cell);
  16:          }
  17:      }
  18:      // 啟動定期更新畫面的執行緒
  19:      Thread t = new Thread(RefreshScreen);
  20:      t.Start(realworld);
  21:      while (_cq.Count > 0)
  22:      {
  23:          Cell item = _cq.GetNextCell();
  24:          if (item.NextWakeUpTime > DateTime.Now)
  25:          {
  26:              // 時間還沒到,發呆一下等到時間到為止
  27:              Thread.Sleep(item.NextWakeUpTime - DateTime.Now);
  28:          }
  29:          ThreadPool.QueueUserWorkItem(RunCellNextStateChange, item);
  30:      }
  31:  }
  32:  private static void RunCellNextStateChange(object state)
  33:  {
  34:      Cell item = state as Cell;
  35:      TimeSpan? ts = item.OnNextStateChangeEx();
  36:      if (ts != null) _cq.AddCell(item);
  37:  }
  38:  private static void RefreshScreen(object state)
  39:  {
  40:      while (true)
  41:      {
  42:          Thread.Sleep(500);
  43:          (state as World).ShowMaps("");
  44:      }
  45:  }

 

GameHost 的工作很明確,一開始 (line 18 ~ 20) 就把更新畫面的動作完全交給另一個執行緒,之後就專心處理 ToDoList 內的工作了。

接著後面的 while loop (line 21 ~ 30) 則是很單純的從 ToDoList 裡取出下一個要要動作的 Cell, 如果時間還沒到就 Sleep 等一下它。執行完後會再詢問下一次是什麼時後,同時再把他加到 ToDoList 內等待下一次輪到他時繼續。

這次的程式我沒有設定停止的條件,因此你會看到程式會不斷的執行下去。程式執行起來,結果跟 #3 沒什麼不同,畫面上的每個細胞會照著題目的規則生長或死亡,不同的是 #3 的 Game Host 需要用到 903 條執行緒,而這版的 Game Host 只要 9 條執行緒...

image

 

其實,以這樣的範例題,我大可以不用顧慮到效能的問題,不過就是示範程式怎麼寫嘛。不過,我的目標如果只是訂在怎麼寫這練習題,大可以 GOOGLE 一下就有一堆作業解答了 :D。我的目標是要展示一下,該如何開發這樣的 GameHost ? 這樣的程式,是大部份的遊戲的基礎,尤其是像線上遊戲或是 facebook 這類互動遊戲的基礎。有了像樣的 Game Host 之後,接下來就把目標放在如何建立多樣的生物,一起放在這世界裡面生活了。接下來就會大量運用到 OOP 的特點 (對,就是上一篇預告的...) 繼承及多型。

有沒有人覺的,這種程式越寫越像 Matrix (就是駭客任務裡的 "母體") 了? 裡面活著的東西其實都在我的掌控之下... =_= 哈哈... 未完待續,請期待續集 :D。

--
範例程式:



9/14/2009 1:32:02 AM

[設計案例] 生命遊戲#2, OOP版的範例程式

Microsoft.NET | C# | 我的作品 | 技術隨筆 | 物件導向 Facebook Share

還好,第一版的程式沒有難產。這版的目的很簡單,就是把題目實作出來,同時我會盡量套用物件導向的理念去設計程式的結構,而不是只把結果算出來而已。其實我一直覺的,這類生命模擬的程式,是非常適合用OOPL來實作的範例,大概OOPL所有強調的特性 (封裝、繼承、多型、動態聯結... 等等) 都用的到,算是完美的應用範例題吧!

不過很奇怪的,我特地 GOOGLE 了一下,不知 OOPL 高手都不屑寫這種範例還是怎樣,找到的範例程式,不管用什麼語言 (C/C++/Java/C#都有) 寫的,清一色都很沒有物件導向的 fu ... 好吧,只好自己來寫一個。

第一步,一定是先看看你的程式,分析出需要那些類別/物件,及它們之間的關係。比較正規的作法就是 UML 的 UseCase 了。不過這範例其實不大,我就直接跳到 Class Diagram 了 (因為VS2008剛好有現成的...)... 主要的類別有兩個: World (世界) 及 Cell (細胞)。

World 就是給 Cell 生活的空間,我們只訂義一個有限大小的二維空間,就一個 M x N 的棋盤這樣。而 Cell 則是一個細胞,描述單一一個細胞本身,在各種不同的條件下會有什麼反應。先貼一下 class diagram:

 

image  
圖1. class diagram (World & Cell)

老實說,這張圖還蠻乏善可陳的,World對外公開的介面,大概包含了幾個主要功能,就是取得指定座標的 Cell (GetCell), 及把目前的整個 World 狀態印出來 (ShowMaps) 的 method 而已。而 Cell 的公開介面,不外乎是它目前是活著還是死的,還有它的建構式,及呼叫後會把狀態轉移到下一次狀態的 method。

其它都是 World / Cell 互相溝通用,或是 Init 用的 Method / Prop, 就不多作介紹。先來看看主程式,扮演上帝的你,如何讓這堆單細胞生物,在你的世界裡活起來:

Game Of Life 主程式[copy code]
   1:  static void Main(string[] args)
   2:  {
   3:      int worldSizeX = 30;
   4:      int worldSizeY = 30;
   5:      int maxGenerationCount = 100;
   6:      World realworld = new World(worldSizeX, worldSizeY);
   7:      for (int generation = 1; generation <= maxGenerationCount; generation++)
   8:      {
   9:          realworld.ShowMaps(string.Format("Generation: {0}", generation));
  10:          Thread.Sleep(1000);
  11:          for (int positionX = 0; positionX < worldSizeX; positionX++)
  12:          {
  13:              for (int positionY = 0; positionY < worldSizeY; positionY++)
  14:              {
  15:                  // do day pass
  16:                  Cell cell = realworld.GetCell(positionX, positionY) as Cell;
  17:                  cell.OnNextStateChange();
  18:              }
  19:          }
  20:      }
  21:  }

 

主程式我還沒把不相干的動作刪掉,也才廿一行... line 1 ~ 5 只是初始值,line 6 建立整個世界,之後就每跑完一個世代 (generation) 就休息一秒鍾,繼續下一次進化。這樣隨著時間的過去,畫面上會一直更新整個世界的狀態... 直到只定的次數到了為止。

 

class World 的部份就沒什麼特別的,就只是把一個二維陣列包裝一下而已。直接貼 Code 就混過去吧 XD,一樣沒有刪掉程式碼,原 CODE 照貼:

class World 的程式碼[copy code]
   1:  public class World
   2:  {
   3:      private int SizeX = 0;
   4:      private int SizeY = 0;
   5:      private Cell[,] _map;
   6:      public World(int maxPosX, int maxPosY)
   7:      {
   8:          this._map = new Cell[maxPosX, maxPosY];
   9:          this.SizeX = maxPosX;
  10:          this.SizeY = maxPosY;
  11:          for (int posX = 0; posX < maxPosX; posX++)
  12:          {
  13:              for (int posY = 0; posY < maxPosY; posY++)
  14:              {
  15:                  this._map[posX, posY] = new Cell(this, posX, posY);
  16:              }
  17:          }
  18:      }
  19:      internal void PutOn(Cell item, int posX, int posY)
  20:      {
  21:          if (this._map[posX, posY] == null)
  22:          {
  23:              this._map[posX, posY] = item;
  24:              item.PosX = posX;
  25:              item.PosY = posY;
  26:          }
  27:          else
  28:          {
  29:              throw new ArgumentException();
  30:          }
  31:      }
  32:      public Cell GetCell(int posX, int posY)
  33:      {
  34:          if (posX >= this.SizeX) return null;
  35:          if (posY >= this.SizeY) return null;
  36:          if (posX < 0) return null;
  37:          if (posY < 0) return null;
  38:          return this._map[posX, posY];
  39:      }
  40:      public void ShowMaps(string title)
  41:      {
  42:          Console.Title = title;
  43:          Console.SetWindowSize(this.SizeX * 2, this.SizeY);
  44:          Console.SetCursorPosition(0, 0);
  45:          Console.Clear();
  46:          for (int y = 0; y < this.SizeY; y++)
  47:          {
  48:              for (int x = 0; x < this.SizeX; x++)
  49:              {
  50:                  Cell item = this.GetCell(x, y);
  51:                  Console.SetCursorPosition(x * 2, y);
  52:                  Console.Write(item.IsAlive? "●":"○");
  53:              }
  54:          }
  55:      }
  56:  }

 

接下來是封裝每個細胞本身跟環境互動的影響,把上一篇講的規則對應成程式碼的樣子。先來看看 CODE:

class Cell 的程式碼[copy code]
   1:  public class Cell //: Life
   2:  {
   3:      protected World CurrentWorld { get; private set; }
   4:      internal int PosX = 0;
   5:      internal int PosY = 0;
   6:      private const double InitAliveProbability = 0.2D;
   7:      private static Random _rnd = new Random();
   8:      public Cell(World world, int posX, int posY) //: base(world, posX, posY)
   9:      {
  10:          this.CurrentWorld = world;
  11:          // setup world
  12:          this.PosX = posY;
  13:          this.PosY = posY;
  14:          this.CurrentWorld.PutOn(this, posX, posY);
  15:          this.IsAlive = (_rnd.NextDouble() < InitAliveProbability);
  16:      }
  17:      public bool IsAlive { get; private set; }
  18:      protected IEnumerable<Cell> FindNeighbors()
  19:      {
  20:          foreach (Cell item in new Cell[] {
  21:              this.CurrentWorld.GetCell(this.PosX -1, this.PosY-1),
  22:              this.CurrentWorld.GetCell(this.PosX, this.PosY-1),
  23:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY-1),
  24:              this.CurrentWorld.GetCell(this.PosX-1, this.PosY),
  25:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY),
  26:              this.CurrentWorld.GetCell(this.PosX-1, this.PosY+1),
  27:              this.CurrentWorld.GetCell(this.PosX, this.PosY+1),
  28:              this.CurrentWorld.GetCell(this.PosX+1, this.PosY+1)})
  29:          {
  30:              if (item != null) yield return item;
  31:          }
  32:          yield break;
  33:      }
  34:      public void OnNextStateChange()
  35:      {
  36:          int livesCount = 0;
  37:          foreach (Cell item in this.FindNeighbors())
  38:          {
  39:              if (item.IsAlive == true) livesCount++;
  40:          }
  41:          if (this.IsAlive == true && livesCount <1)
  42:          {
  43:              //孤單死亡:如果細胞的鄰居小於一個,則該細胞在下一次狀態將死亡。
  44:              this.IsAlive = false;
  45:          }
  46:          else if (this.IsAlive == true && livesCount >= 4)
  47:          {
  48:              //擁擠死亡:如果細胞的鄰居在四個以上,則該細胞在下一次狀態將死亡。
  49:              this.IsAlive = false;
  50:          }
  51:          else if (this.IsAlive == true && (livesCount == 2 || livesCount == 3))
  52:          {
  53:              //穩定:如果細胞的鄰居為二個或三個,則下一次狀態為穩定存活。
  54:              //this.IsAlive = true;
  55:          }
  56:          else if (this.IsAlive == false && livesCount == 3)
  57:          {
  58:              //復活:如果某位置原無細胞存活,而該位置的鄰居為三個,則該位置將復活一細胞。
  59:              this.IsAlive = true;
  60:          }
  61:          else
  62:          {
  63:              // ToDo: 未定義的狀態? assert
  64:          }
  65:      }
  66:  }

這裡開始應用到 OOPL 第一個特性: 封裝。從程式碼可以看到,主要的邏輯都被包在裡面了,就 Game Of Life 裡提到的四條規則。

程式這樣寫起來,比那些作業的標準答案看起來舒服多了吧? 雖然行數多了一些,不過看起來比較有 OO 的樣子了。當然只是看起來爽是沒用的,這樣的架構,到目前為只除了邏輯清楚一點之外,還看不到其它很明顯的好處。不過當這個規責稍微複雜一點,OOPL的優點就會被突顯出來了。

下回,把題目做點變化,再來看看程式該如何調整…   ((待續))

--
附件: 範例程式碼



4/17/2009 7:51:00 PM

RUNPC 精選文章 - 運用ThreadPool發揮CPU運算能力

543 | C# | RUNPC | Threading | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章] Facebook Share

果然這個什麼東西都上網的年代,要三不五時的 GOOGLE 一下自己,才會知道那些網站把你的八卦跟內幕爆了出來... 不過應該沒啥週刊記者對我有興趣吧? 哈哈。在 GOOGLE 自己名字時,倒是意外發現,之前投稿的文章,又有一篇被拿來登在網站上的精選文章了 :D

特此留念一下 :D

http://www.runpc.com.tw/content/main_content.aspx?mgo=176&fid=E02

--

順便整理一下懶人包:

另一篇精選文章 [RUN!PC 精選文章 - 生產線模式的多執行緒應用]

過去投過的系列文章 (multi-threading programming using c#):

2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式



1/16/2009 2:10:00 AM

RUN!PC 精選文章 - 生產線模式的多執行緒應用

543 | C# | RUNPC | Threading | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章] Facebook Share

http://www.runpc.com.tw/content/main_content.aspx?mgo=178&fid=E08

無意間 search 我自己的名字,才發現這篇文章除了投稿到 RUN! PC 之外,原來還有刊在網站上的精選文章啊...

哈哈,暗爽一下,順道貼一下 link, 讓沒看到雜誌的網友們也有機會看一看在下的作品...



12/7/2008 2:04:10 PM

原來 .NET 早就內建 XmlNodeWriter 了...

Microsoft.NET | C# | XML | 小技巧 | 我的作品 | 技術隨筆 | 物件導向 Facebook Share

最近事情一堆,上班忙上班的事,下班還在忙著研究 Enterprise Library, Entity Framework, 還有一堆五四三的,文章寫的就少了... 先跟有訂閱我 BLOG 的朋友們說聲道歉...。 不過在寫新專案的過程中,意外的發現這東西,一定要提一下...

 

不知道有多少人用過 XmlNodeWriter ? 我用這東西用很久了,當年 Microsoft 推出 .NET Framework 時,強調有很強的 XML 處理能力,其中 XmlReader / XmlWriter 就是以效能為考量,讓你避開處理大型 XML 資料效能很糟糕的 XmlDocument, 也不用去碰很難寫的 SAX 的替代方案...

無奈 M$ 內建的 XmlWriter 少的可憐,只能寫到檔案或是 TextWriter ... 看看權威的 MSDN 告訴我們有那些 XmlWriter 可以用?

image

老實說除了 XmlTextWriter 之外,另外兩個很少用的到。XmlWriter 在輸出 XML 時很好用 (如果你只作輸出的話),複雜的 XML 輸出用 XmlWriter 比用 XmlDocument 簡單多了,不過最常碰到的情況是我還是想用 XmlDocument 來操作 XML,不過其中一部份的 NODE 想用 XmlWriter 來更新內容...

古早有位好心的 MVP 寫了 XmlNodeWriter, 就可以讓我這樣用:

XmlNodeWriter Sample Code:[copy code]
   1:  XmlDocument xdoc = new XmlDocument();
   2:  xdoc.LoadXml("<root><node1><data/><data/><data/></node1><node2/></root>");
   3:  XmlNodeWriter xnw = new XmlNodeWriter(xdoc.DocumentElement, true);
   4:  xnw.WriteStartElement("newNode");
   5:  xnw.WriteAttributeString("newatt", "123");
   6:  xnw.WriteCData("1234567890");
   7:  xnw.WriteEndElement();
   8:  xnw.Close();
   9:  xdoc.Save(Console.Out);

 

第一個參數是 XmlNode, 第二個參數是要不要清掉原來 Node 下的內容。很棒,我可以直接拿 XmlNode 當作 XmlWriter 輸出的對象,透過 Writer 寫出去的東西就直接反映在 XmlNode 身上了,省掉輸出成 Text 然後再 PARSING 回 XML NODE 這種蠢事...

 

前面只是緬懷 XmlNodeWriter 到底有多好用,現在找不到有多難過而已... 接下來才是正題...

 

不過現在想再去找 XmlNodeWriter 官方網站已經找不到了 @_@,原本這 lib 是 hosting 在 gotdotnet.com 這網站上,不過 M$ 已經把它關了,改成 codeplex.com / MSDN Code Gallery 取代,只好求助 GOOGLE 大神,無意間又發現這 M$ XmlTeam 的 BLOGCOMMENTS 有這麼一段:

 

# re: XML Features in the February CTP of Visual Studio “Orcas”
Friday, February 02, 2007 8:27 PM by Stuart Ballard


Is there going to be an XmlNodeWriter in Orcas? It's a fairly glaring hole, especially if you've ever wanted to apply an XSL transformation to an in-memory XmlDocument and get the result as another in-memory XmlDocument. You can pass the input to the transform via XmlNodeReader, but to get it back out again you just have to feed your XmlWriter to a StringBuilder and then parse it...

Fortunately my use case wasn't performance-critical, but it's still ugly...

 

# re: XML Features in the February CTP of Visual Studio “Orcas”
Saturday, February 03, 2007 3:22 AM by Oleg Tkachenko


Stuart, XmlNodeWriter in .NET 2.0 is hiding in

xmlNode.createNavigator().AppendChild() method. It can be used to populate XmlNode via XmlWriter API and so you can

XmlDocument doc = new XmlDocument();

using (XmlWriter writer = doc.CreateNavigator().AppendChild()) {

   xslt.Transform(input, (XsltArgumentList)null, writer);

}

Mike, am I right that  Orcas January CTP includes none of these coolness?

 

真是太機車了,這麼好用的東西藏在這種地方? @_@,枉我從 .NET 1.0 beta 就開始用 C# 處理 XML,連 XSLT Extension 都寫過一堆, Trace Code 也追到 XSLT 內抓過一堆問題... 竟然連這東西都沒發現? 可惡...

於是手又癢了,拿來試用看看,發現只要動手寫幾行 Code, 我就能把 XmlNodeWriter 變回來了,像這樣:

 

我的 XmlNodeWriter 實作[copy code]
   1:  public class XmlNodeWriter : XmlWriter
   2:  {
   3:      private XmlWriter _inner_writer = null;
   4:      public XmlNodeWriter(XmlNode node, bool clean)
   5:      {
   6:          if (clean == true)
   7:          {
   8:              node.RemoveAll();
   9:          }
  10:          this._inner_writer = node.CreateNavigator().AppendChild();
  11:      }
  12:      #region 無聊的 "延長線" 程式碼...
  13:      // 略! 共一百多行,補上廿幾個 abstract method / property, 把它接到 _inner_writer 上
  14:      #endregion
  15:  }

 

這樣果真可以 WORK 了 :D  不過要真正變出一個新的 XmlNodeWriter 代價還不低,繼承 XmlWriter 的後果是有廿幾個 abstract method / property 得補上實作... 全都是很無聊的 code, 就是拿 _inner_writer 的直接套上去而已... 像這樣:

"延長線" 型的程式碼[copy code]
   1:  public override void Close()
   2:  {
   3:      this._inner_writer.Close();
   4:  }
   5:  public override void Flush()
   6:  {
   7:      this._inner_writer.Flush();
   8:  }
   9:  public override string LookupPrefix(string ns)
  10:  {
  11:      return this._inner_writer.LookupPrefix(ns);
  12:  }

 

這堆 Code 我就不貼了,總之可以 WORK :D

 

image

 

不過對 CODE 有點潔癖的我,越看越不是味道,就動起 Factory 的腦筋了。繼續改造一下... 原本 .NET 2.0 內建的 XmlWriter 就已經提供 Factory 的用法了,像這樣:

XmlWriter my_writer = XmlWriter.Create( ... );

不過沒辦法不改 .NET FX 原始碼的情況下 "加掛" 我自己的 Create(...) 實作,原本腦筋是動到 C# 3.0 開始支援的 Extension Method, 不過它只支援 instance method, 不支援 static method ... 只好改成這樣:

 

XmlWriterFactory 實作[copy code]
   1:  public abstract class XmlWriterFactory : XmlWriter
   2:  {
   3:      public static XmlWriter Create(XmlNode node)
   4:      {
   5:          return Create(node, false, null);
   6:      }
   7:      public static XmlWriter Create(XmlNode node, bool clearnContent)
   8:      {
   9:          return Create(node, clearnContent, null);
  10:      }
  11:      public static XmlWriter Create(XmlNode node, bool cleanContent, XmlWriterSettings settings)
  12:      {
  13:          if (node == null) throw new ArgumentNullException("node");
  14:          if (cleanContent == true)
  15:          {
  16:              node.RemoveAll();
  17:          }
  18:          XmlWriter xw = node.CreateNavigator().AppendChild();
  19:          if (settings != null)
  20:          {
  21:              xw = XmlWriter.Create(xw, settings);
  22:          }
  23:          return xw;
  24:      }
  25:  }

 

沒事還繼承原本的 XmlWriter 只有一個目的,就是要延用它原來的 10 種 Create method 啊... 貼張圖為証,繼承之後我就有 13 種不同的 Create method 可以用... 不用再兩頭跑 (只是不能加在原本的 XmlWriter 上真是殘念, C# 什麼時後會支援 static method extension ?):

image

 

 

當然,原程式只要改掉如何拿到 XmlWriter 那行而已,其它都照舊就可以執行了 :D

 

有需要的就拿去用吧,CODE 才十幾行,還包成 DLL 實在太麻煩了,需要的直接貼到你自己的 CODE 裡就好! 要散怖都隨便,沒有什麼授權問題,唯一的要求就是讓我知道我的 CODE 你有在用就好 :D,想讚助我的也很簡單,BLOG 上該多點幾下的東西,沒事就點一點... 哈哈



11/4/2008 2:11:00 AM

[RUN! PC] 2008 十一月號

Microsoft.NET | RUNPC | Threading | 我的作品 | 技術隨筆 Facebook Share

IMG_0208

YA! 第四篇!! :D 還是一樣要先感謝一下編輯賞光,讓我有點空間寫些不一樣的東西。

 

基本的執行緒相關的程式設計跟函式庫,講的差不多了,其實這些也沒什麼好寫的。接下來打算寫一些應用的模式,來談談有那些方法,那些設計方式才能夠有效的發揮多執行緒的優點。看了 .NET Framework 4.0 / Visual Studio 2010 的 ROADMAP,有一大部份的重點擺在平行處理,INTEL年底也要發表四核 + HT 的 CPU ( WINDOWS 會認為有八個處理器 ),軟硬體都備齊了,剩下的就是程式設計師的巧思了。

 

其實之前貼過幾篇類似主題的文章,只是這次把它統合起來介紹一下。生產線模式,如果簡化後就是 [生產者消費者] 的模式,而把它徹底一點的應用,則是上回提到 [Stream Pipeline] ..

這篇也是第一次在雜誌上嘗試說明比較偏設計概念的文章,實作比較少,很怕不合讀者的口味... 應該不會貼了就沒續篇了吧? :P 有買雜誌的記得讀者回函填一下,哈哈,也算是點鼓勵。這次範例程式也是 Console application (我不會寫太炫的程式 :P ),需要的可以點 [這裡] 下載!



11/3/2008 2:34:00 AM

該如何學好 "寫程式" #5. 善用 TRACE / ASSERT

[精選文章] | 543 | C# | Microsoft.NET | 我的作品 | 技術隨筆 | 物件導向 Facebook Share

哈哈,這篇拖的夠久了 :P

上篇扯太多,寫到一半寫不完就留到這篇了。寫出可靠的程式,這是軟體工程師的基本要求。上篇提到了 TRACE / ASSERT 的應用,來複習一下:

TRACE: 原本是 C 的除錯用巨集,目的是用適合的方式輸出除錯用的訊息,用來跟一般的訊息輸出有所區別。因為用的是不同的方式輸出,可以很容易的統一關掉。隨著工具的進步,輸出的方式也越來越適合除錯,比如輸出到開發工具的除錯視窗,或是輸出成記錄檔等等。

ASSERT: 也是除錯用巨集,它接受一個 bool 參數,輸入值為 TRUE 時一切正常,就像沒呼叫一樣,輸入 FALSE 則會中斷程式,或是輸出顯目的警告訊息。目的在於確保程式的每個步驟情況都如預料般的順利。

這兩個東西從 C 的巨集,衍生出各種語言及環境都有各自的版本。它的目的很簡單,就是 [Writing Solid Code] 裡提到的:

用同一套程式碼,同時維護兩個版本 (RELEASE / DEBUG),讓錯誤自動跑出來

 

雖然這本書提到了不少技巧,正確的應用 TRACE / ASSERT 是最基本的。但是那些細節並不是主要的重點。重點是你在寫 CODE 時有時時刻刻記得要盡量減少 BUG 嗎? 你有正確的擬出對策嗎? 來看看上回最後一段範例程式:

 

加上 ASSERT 的算分程式碼[copy code]
        public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)        {            int totalScore = 0;            int itemCount = quiz_question.SelectNodes("item").Count;            Trace.Assert(quiz_question != null);            Trace.Assert(paper_question != null);            Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);            //            //  如果都沒作答, 此題放棄            //            if (paper_question.SelectNodes("item[@checked='true']").Count == 0)            {                return 0;            }            //            //  題目的配分            //            int quiz_score = int.Parse(quiz_question.GetAttribute("score"));            //            //  答對一個選項的分數            //            int item_score = quiz_score / itemCount;            for (int itemPos = 0; itemPos < itemCount; itemPos++)            {                XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;                XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;                //                //  算成積                //                if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))                {                    totalScore += item_score;                }                else                {                    totalScore -= item_score;                }            }            Trace.Assert(totalScore >= (0 - quiz_score));            Trace.Assert(totalScore <= quiz_score);                        return totalScore;        }
   1:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
   2:  {
   3:      int totalScore = 0;
   4:      int itemCount = quiz_question.SelectNodes("item").Count;
   5:      Trace.Assert(quiz_question != null);
   6:      Trace.Assert(paper_question != null);
   7:      Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);
   8:      //
   9:      //  如果都沒作答, 此題放棄
  10:      //
  11:      if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
  12:      {
  13:          return 0;
  14:      }
  15:      //
  16:      //  題目的配分
  17:      //
  18:      int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
  19:      //
  20:      //  答對一個選項的分數
  21:      //
  22:      int item_score = quiz_score / itemCount;
  23:      for (int itemPos = 0; itemPos < itemCount; itemPos++)
  24:      {
  25:          XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
  26:          XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
  27:          //
  28:          //  算成積
  29:          //
  30:          if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
  31:          {
  32:              totalScore += item_score;
  33:          }
  34:          else
  35:          {
  36:              totalScore -= item_score;
  37:          }
  38:      }
  39:      Trace.Assert(totalScore >= (0 - quiz_score));
  40:      Trace.Assert(totalScore <= quiz_score);
  41:      return totalScore;
  42:  }

 

各位仔細看一下加上 ASSERT 的地方。大家寫程式,通常都是腦袋裡想著 "我要處理什麼問題" ,很少人會去想錯誤處理的部份。沒錯,這部份的確是吃力不討好,以此例來說,光是傳進來的參數就有可能狀況百出了。正常的流程都寫不完了,誰還有力氣去把這些錯誤都擋下來?

不過最容易出錯的地方也在這裡。我常在跟其它工程師說,正確的資料 (參數) 傳進來,本來就應該有正確的答案傳出去。難的是錯誤的資料傳進來,你還得回應 "正確" 的錯誤訊息回去,這才真的是個挑戰。這時 ASSERT 的效果就出來了。你可以把 ASSERT 想像成 "宣告" 的子句。以 line 5 ~ 7 行為例:

確保傳入參數是正確的[copy code]
            Trace.Assert(quiz_question != null);            Trace.Assert(paper_question != null);            Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);
   1:  Trace.Assert(quiz_question != null);
   2:  Trace.Assert(paper_question != null);
   3:  Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);

 

這三行看在我眼裡,意思就是:

"這兩個參數不能是 NULL,而且兩個 XML ELEMENT 都要有一樣數量的子節點 (Element),否則就不惜代價警告我"

同樣的,在程式的中間,還有傳回值之前,也都可以用同樣的方式來替你的程式 "把關"。再來看看算完成績後,要把值傳回去之前的 CODE:

確保傳回值的範圍正確的程式碼[copy code]
            Trace.Assert(totalScore >= (0 - quiz_score));            Trace.Assert(totalScore <= quiz_score);                        return totalScore;
   1:  Trace.Assert(totalScore >= (0 - quiz_score));
   2:  Trace.Assert(totalScore <= quiz_score);
   3:  return totalScore;

 

這兩行的意思就是:

"不管成績怎麼算,每張答案卷最後的總分一定介於 0 ~ 滿分之間。一樣,有例外的話就不惜代價警告我"

 

聽起來蠻狠的,不惜代價...,不過使用 ASSERT 的話就真的是這樣。通常碰到 ASSERT 後,程式不是進 DEBUGGER 就是直接關掉了。不過請大家注意一下,並不是到處加上 ASSERT 你的程式就沒問題了。要搞清楚加上它的目的是什麼。它要抓的是你程式的 BUG,不是執行期的錯誤 (比如 USER 輸入錯誤的值,或是必填的資料沒填等等)。執行期的錯誤,你還是得乖乖的寫程式,不能用 ASSERT 替代。

舉例來說,如果最後算出來的分數是負的,則會觸動 return 前的 ASSERT。有些有點經驗又有點兩光的 PROGRAMMER 可能會自己顯示一些錯誤訊息。但是這跟本不干 USER 的事啊! 會出現這種情況,錯的一定是 "程式" 本身,也就是你看到 ASSERT 警告後就該來改程式抓 BUG 了。加上 ASSERT 的目的就是在你的程式到處布下眼線,任何一個地方偵測到不對勁,馬上通知你來處理。

當你有心把程式寫好時,你才會覺的這樣作是必要的,而不是累贅。你眼線布的越多,BUG就越難藏在你的程式裡。相對的,如果傳進來的參數就不對了,那應該怎麼辦?

這時就要小心分清楚你要抓的是 BUG 還是做錯誤處理了。如果參數是 USER 直接輸入的,那收到 NULL 或是錯誤的值本來就有可能 (吃芝蔴那有不掉燒餅的...),你需要的是老老實實寫好錯誤處理的流程。但是如果你的 API 早已嚴格定義不接受 NULL,卻還是有白目的工程師硬把 NULL 傳給你的 API,那這時就是 BUG 了,應該用 ASSERT 抓出來,然後找到冤大頭叫他改程式。

不過這樣的 CODE 可不能交到 USER 手上。想像一下如果你正在用 WORD 打文件,結果碰到一個小 BUG,ASSERT 就跳出警告訊息要中止程式,你連存檔都來不及,大概會抓狂吧。這時就是一份程式碼兩種版本的作法發威的地方了。交給 USER 的程式,就應該是切到 RELEASE MODE (或是關掉 ASSERT / TRACE) 編譯的版本。這時所有的 TRACE / ASSERT 好像完全消失一樣,程式就如同一般情況運作。

當 USER 回報一些很難抓到的 BUG 時,這時就可以打開 ASSERT 或是改用 DEBUG BUILD 的版本,再讓 USER 去重現 BUG,這時如果你都有老老實實加上 ASSERT 的話,BINGO,問題在那就一目了然。看看是那一道 ASSERT 指令被觸發,就知道是什麼問題了。抓 BUG 最麻煩的就是找出錯在那裡,而善用 ASSERT 就可以讓 BUG 自己跳出來告訴你出了什麼問題,只要你養成好習慣。

 

再舉一個應用例。看到 Steve Maguire 先生舉這個例子,真是想拍手叫好。他舉了他們在開發 EXCEL 時的例子。EXCEL就是要替試算表作一堆運算,當年還在 DOS 時代,CPU怎樣都不夠快,RAM怎樣都不夠多,程式設計師無不絞盡腦汁,要榨出所有的運算能力,最佳化做到無所不用其極的地步。不過這種東西是錯不得的啊,少算了一塊錢還得了? 碰到這種問題你該怎麼辦?

通常,我們都會先有個安全的版本,算的不快,但是因為邏輯簡單,比較不容易出錯。這種版本寫出來後才開始想盡辦法,去改善程式讓速度加快。馬先生 (ㄜ... 是馬奎爾先生... ) 就充份應用了 ASSERT,隨時都要把 BUG 逼出來的精神,真的把 "驗算" 的方法應用上來。它的作法很簡單,同一張試算表,用兩份不同的程式碼各計算一次,最後再來比對一下結果 (驗算)。只要兩者得到的答案不一樣,那就是出問題了! 當然也有可能是安全的版本寫錯了,不過你至少多了個機會抓到問題,因為不一樣的話,一定 "至少" 有一邊是錯的!

 

沒有這樣的前題的話,各位看到可能都會在心裡想:

"有沒有搞錯,程式都寫不完了,還要寫兩種演算法來驗算?? 老闆又不會多給我一點薪水..."

沒錯,這的確是成本較高的方法,每套系統應該都有關鑑的地方,只要有絕對不能失誤的地方,就值得用這種作法。速度的問題怎麼辦? 很簡單。你只要在 DEBUG MODE 才啟用這 "驗算" 的機制,測試人員輸入各種數值做黑箱測試,如果每次測試的過程中發現驗算錯誤,則 "黑箱" 測試就能幫助你抓到只有 "白箱" 測試才有可能抓到的 BUG !

 

我寫的這個範例程式 (算成績) 其實也準備了兩個版本。上一篇貼的是基本的作法,結果比較可靠。而為了效率我也寫了另一份程式碼,用的是位元運算,希望藉著位元運算,一次就把多選題的答案給算出來。開發的過程中就用了 ASSERT + 驗算的技巧,它不會加快我寫程式的速度,但是它可以加速我找到 BUG 跟解決 BUG 的時間!

 

有沒有覺的這跟單元測試其實很像? 沒錯。單元測試就是一樣的觀念演變出來的作法,所以你用的單元測試 FRAMEWORK 也延用一樣的 ASSERT 使用慣例。你會發現其實之間的觀念都是相通的,只不過單元測試更進一步的把它系統化了,由原本四處藏在程式碼中的 ASSERT,抽出來成為一個一個獨立的 TEST CASE,由原本被動的執行時期檢查,演變為主動執行所有測試的 UNIT TEST。我覺的 Kent 在 XP (extreme programming) 裡舉了一個例子來說明單元測試,比喻的很貼切,我覺的也一樣能拿來比喻 ASSERT:

 

"車子裝了煞車,是要讓車子能開的更快!"

 

聽起來好像很蠢? 煞車明明是讓車子停下來的... 其實不然。想像一下如果你的車子沒煞車,你敢開多快? 了不起就是撞到不會怎麼樣的速度,或是油門放開就停下來的程度而以。有了煞車讓你有信心,碰到危險時你隨時能把車子停下來,你才敢把車子開上高速公路...

 

很有道理的比喻,ASSERT 跟 UNIT TEST 大部份人都覺的是 "煞車",是拖慢你速度用的,但是也因為有這些 "煞車",你才能放心的衝更快。當你有充份運用 ASSERT 的話,你就能很放心的寫程式,沒有後顧之憂。其實類似的關念,Steve Maguire 的書還有提到很多,只不過它的範例都是用 C 寫的 (還不是 C++ ...),看起來會吃力一點。範例程式可能對現今大部份的人都用不到,但是裡面的觀念跟作法還是很有參考價值的,手上還有這本書的人不妨拿起來翻一翻。

 

講到這裡,花了兩篇才講完第一個部份,主要的重點就是用 TRACE / ASSERT 來說明,要讓你的程式夠穩定,第一個要改進的就是你寫程式的想法,觀念及態度。各位不妨以這兩篇的例子,自己回想看看,你做到那幾項:

  1. 你寫程式有考慮到這些問題嗎?
  2. 如果你寫程式有用這些方法,有多少你曾解過的棘手 BUG 會變的迎刃而解的?
  3. 加上 ASSERT 之後,你是否對你程式更有信心了?
  4. 你是否更認同單元測試的必要了?

 

想法跟觀念有了改變,才有可能開發出優良的軟體。你開始認同這樣的想法了嗎? 恭喜你,你已經跨出第一步了。不過光是 BUG FREE 還不足以成為優秀的軟體工程師,這只是必要條件之一而以。除了把程式寫的 "可靠" 之外,接下來的挑戰是如何把程式寫的 "漂亮" ? 下回要開始來探討如何構思你程式碼的結構。什麼樣的結構,什麼樣的方式去分析你的問題,才寫的出架構漂亮的程式? 別急,請期待續篇 :D

 

 

--

註: 範例程式很多 CODE 被我跳過去了,有興趣的人可以抓回去研究看看... 請點 [這裡] 下載。



10/20/2008 1:53:00 AM

該如何學好 "寫程式" #4. 你的程式夠 "可靠" 嗎?

543 | C# | TROUBLE SHOOTING | 小技巧 | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章] Facebook Share

撐了很久,續篇來了。這次要進階一點,直接從 software engineer (軟體工程師) 的階段開始。

 

所謂的軟體工程師,我對它的定義是在這個領域已經算是資深人員了。programmer 該作的是把程式寫好,要挑正確的方式及技術寫好你的程式 (如之前幾篇介紹的演算法及資料結構等等)。而軟體工程師呢? 之前介紹的那些已經不夠了,你該好好的安排你的 code 及工具,要能把你的 solution (如會用到的演算法及資料結構),跟你手上能運用的資源 (如程式語言、開發工具及函式庫) 作最佳化的搭配及整合。因此我認為在這階段的重點有幾個:

  1. 先成為一個好的 programmer (廢話)
  2. 程式要有足夠的可靠性 (穩定、沒有BUG、易讀、對於未知問題的防禦能力)
  3. 要有足夠的系統知識 (比如作業系統/API/系統服務/記憶體管理等等 OS 提供的環境及功能)
  4. 程式要有好的結構 (正確/優秀的類別設計、好維護、有足夠的擴充及應變能力)
  5. 要有解決未知問題或是未知 BUG 的能力,有自行學習新知的能力。

這些能力,跟 programmer 需要俱備的剛好是另一個角度的要求。某種程度上是各自發展的,不會互相衝突。有心的 programmer 應該要及早作好準備。如果 programmer 是要把程式寫對,那 software engineer 就是要把程式寫好,用專業的方式來寫,而不是用業餘的方式。

什麼叫作 "專業" 的程式? 我舉幾個例子,你的程式防呆嗎? 你的程式面對未知的問題或狀況的免疫力夠不夠強? 面對問題時你的程式有沒有比其它人的程式還容易抓出 BUG ? 你有能力有系統的找出未知的問題嗎? 還是只能看著程式碼發呆? 面對上面的問題有沒有有效的預防措施? 設計階段可以怎樣預防? ... etc

實在太多了。不過這些看起來又是教條,實際上這幾點會影響的到底是什麼? 後面幾篇就一項一項來看吧!

 

[程式要有足夠的可靠性]

老實說,我很怕光是這一段,就會拖到好幾篇了 ... @_@,我會盡量挑出重點來寫。開始之前先問一下,不知道有多少人看過馬奎爾 (Steve Maguire) 寫的這本書? 有的話記得留個回應 :D

"完美程式設計指南" (Writing Solid Code)

這本書真是經典。不過它真的也很 "經典",是 1993 年就出版的書。以講程式設計來說,這個年代的書真的可以扔了,裡面的範例現在沒幾個人用的到了。不過它提到的作法真的是很實際,只是書上的範例大半都過時了,下面碰到的例子我都會用 C# 重新表達一次作者的理念。在這個主題我就舉幾個例子,各位讀者可以自己回顧一下你的程式碼,到底藏了多少地雷在裡面?

 

[要讓問題浮現出來: 善用 DEBUG / RELEASE 模式]

專不專業就看這裡了。如果你想當個稱職的軟體工程師,除了讓程式跑的快之外,第一點就是要降低 BUG 數。如果你面對 BUG 的態度是 "找到再改就好",或是 BUG 一堆你也沒方法去預防,也沒辦法降低 BUG 出現的頻率,那麼你跟半路出家的人差別在那?

大家都知道 Visual Studio 正上方就有個切換 Release / Debug 模式的選單吧? 你確切瞭解它是幹嘛的嗎? 先從一個簡單的範例開始吧! 我工作上常碰到線上測驗之類的應用軟體開發,因此線上考試算分是個很常用的功能。因此我把這個重責大任交給工程師來處理。先來看看我要求工程師寫什麼 CODE ? 我用 XML 定義了一份考卷 (QUIZ.xml,含正確答案),也定義了答案卷的格式 (PAPER-XXXX.xml),程式很簡單,就是拿到題目跟答案卷後,要算出正確的總分。

不難吧? 先看看 XML 檔長啥樣子:

 

試卷 (QUIZ.xml)[copy code]
<?xml version="1.0" encoding="utf-8" ?><quiz>	<question score="20">		<body>那一隻熊最勵害?</body>		<item correct="false">白熊</item>		<item correct="false">黑熊</item>		<item correct="false">棕熊</item>		<item correct="true">灰熊</item>	</question>	<question score="40">		<body>誰發現萬有引力?</body>		<item correct="false">鼠頓</item>		<item correct="true">牛頓</item>		<item correct="false">虎頓</item>		<item correct="false">兔頓</item>	</question>	<question score="40">		<body>下列那些東西是可以吃的?</body>		<item correct="false">東瓜</item>		<item correct="true">西瓜</item>		<item correct="true">南瓜</item>		<item correct="false">北瓜</item>	</question></quiz>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question score="20">
   4:      <body>那一隻熊最勵害?</body>
   5:      <item correct="false">白熊</item>
   6:      <item correct="false">黑熊</item>
   7:      <item correct="false">棕熊</item>
   8:      <item correct="true">灰熊</item>
   9:    </question>
  10:   
  11:    <question score="40">
  12:      <body>誰發現萬有引力?</body>
  13:      <item correct="false">鼠頓</item>
  14:      <item correct="true">牛頓</item>
  15:      <item correct="false">虎頓</item>
  16:      <item correct="false">兔頓</item>
  17:    </question>
  18:   
  19:    <question score="40">
  20:      <body>下列那些東西是可以吃的?</body>
  21:      <item correct="false">東瓜</item>
  22:      <item correct="true">西瓜</item>
  23:      <item correct="true">南瓜</item>
  24:      <item correct="false">北瓜</item>
  25:    </question>
  26:  </quiz>

 

 

再來代表答案卷的檔案 (PAPER-PERFECT.xml),這份看來是天才寫的,每一題都答對了... @_@

答案卷 (都是正確答案,PAPER-PERFECT.xml)[copy code]
<?xml version="1.0" encoding="utf-8" ?><quiz>	<question>		<item checked="false" />		<item checked="false" />		<item checked="false" />		<item checked="true" />	</question>	<question>		<item checked="false" />		<item checked="true" />		<item checked="false" />		<item checked="false" />	</question>	<question>		<item checked="false" />		<item checked="true" />		<item checked="true" />		<item checked="false" />	</question></quiz>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question>
   4:      <item checked="false" />
   5:      <item checked="false" />
   6:      <item checked="false" />
   7:      <item checked="true" />
   8:    </question>
   9:    <question>
  10:      <item checked="false" />
  11:      <item checked="true" />
  12:      <item checked="false" />
  13:      <item checked="false" />
  14:    </question>
  15:    <question>
  16:      <item checked="false" />
  17:      <item checked="true" />
  18:      <item checked="true" />
  19:      <item checked="false" />
  20:    </question>
  21:  </quiz>

 

 

而我交待的算分規則也很簡單,就一般考試的計算方式: 每題有自己的配分,以複選題來算,答對幾個選項就照比例給分,答錯會倒扣。新人工程師果然好用耐操,不一會就交給我這份 Library 的程式碼:

第一版計分程式[copy code]
        public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)        {            int questionCount = quizDoc.SelectNodes("/quiz/question").Count;            int totalScore = 0;            for (int questionPos = 0; questionPos < questionCount; questionPos++)            {                XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;                XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;                totalScore += ComputeQuestionScore(quiz_question, paper_question);            }            return totalScore;        }        public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)        {            int totalScore = 0;            int itemCount = quiz_question.SelectNodes("item").Count;            //            //  題目的配分            //            int quiz_score = int.Parse(quiz_question.GetAttribute("score"));            //            //  答對一個選項的分數            //            int item_score = quiz_score / itemCount;            for (int itemPos = 0; itemPos < itemCount; itemPos++)            {                XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;                XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;                //                //  算成積                //                if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))                {                    totalScore += item_score;                }                else                {                    totalScore -= item_score;                }            }                        return totalScore;        }
   1:  public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
   2:  {
   3:      int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
   4:      int totalScore = 0;
   5:      for (int questionPos = 0; questionPos < questionCount; questionPos++)
   6:      {
   7:          XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   8:          XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   9:          totalScore += ComputeQuestionScore(quiz_question, paper_question);
  10:      }
  11:      return totalScore;
  12:  }
  13:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
  14:  {
  15:      int totalScore = 0;
  16:      int itemCount = quiz_question.SelectNodes("item").Count;
  17:      //
  18:      //  題目的配分
  19:      //
  20:      int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
  21:      //
  22:      //  答對一個選項的分數
  23:      //
  24:      int item_score = quiz_score / itemCount;
  25:      for (int itemPos = 0; itemPos < itemCount; itemPos++)
  26:      {
  27:          XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
  28:          XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
  29:          //
  30:          //  算成積
  31:          //
  32:          if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
  33:          {
  34:              totalScore += item_score;
  35:          }
  36:          else
  37:          {
  38:              totalScore -= item_score;
  39:          }
  40:      }
  41:      return totalScore;
  42:  }

 

很中規中舉的程式,把天才寫的答案卷 (paper-perfect.xml) 套進去算,也真的拿到滿分,於是工程師就很高興的把程式 shelve 給我...

各位回頭想想上面的問題。這段程式以作業的標準來說勉強及格了。但是以實際系統運作的角度來說有那些缺陷?

原則上程式只要是人寫的都會有 BUG,不過我也是人,沒辦法一眼看穿所有程式的問題... 只能事事抱著懷疑的態度,試一試再說。我不是天才,所以寫不出滿分的答案,我另外準備了一份答案卷 (PAPER-NORMAL1.xml):

只答對第一題的答案卷 (PAPER-NORMAL1.xml)[copy code]
<?xml version="1.0" encoding="utf-8" ?><quiz>	<question>		<item checked="false" />		<item checked="false" />		<item checked="false" />		<item checked="true" />	</question>	<question>		<item checked="false" />		<item checked="false" />		<item checked="false" />		<item checked="false" />	</question>	<question>		<item checked="false" />		<item checked="false" />		<item checked="false" />		<item checked="false" />	</question></quiz>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question>
   4:      <item checked="false" />
   5:      <item checked="false" />
   6:      <item checked="false" />
   7:      <item checked="true" />
   8:    </question>
   9:    <question>
  10:      <item checked="false" />
  11:      <item checked="false" />
  12:      <item checked="false" />
  13:      <item checked="false" />
  14:    </question>
  15:    <question>
  16:      <item checked="false" />
  17:      <item checked="false" />
  18:      <item checked="false" />
  19:      <item checked="false" />
  20:    </question>
  21:  </quiz>

 

見鬼了,算出來是 40 分... 蠢才也是有尊嚴的,不用平白無故送我 20 分吧... @_@,我把 BUG 丟回去給工程師,最後他抓出 BUG 在那裡了,第二題第三題我完全沒作答,應該視為放棄才對,結果程式也照規則給我算分... 運氣好多賺了 20 分.. 工程師又改了一版給我,這次加上了放棄此題的判斷 (第八行):

修正後的程式 #2: 放棄的話不算分[copy code]
        public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)        {            int totalScore = 0;            int itemCount = quiz_question.SelectNodes("item").Count;            //            //  如果都沒作答, 此題放棄            //            if (paper_question.SelectNodes("item[@checked='true']").Count == 0) return 0;            //            //  題目的配分            //            int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
   1:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
   2:  {
   3:      int totalScore = 0;
   4:      int itemCount = quiz_question.SelectNodes("item").Count;
   5:      //
   6:      //  如果都沒作答, 此題放棄
   7:      //
   8:      if (paper_question.SelectNodes("item[@checked='true']").Count == 0) return 0;
   9:      //
  10:      //  題目的配分
  11:      //
  12:      int quiz_score = int.Parse(quiz_question.GetAttribute("score"));

 

 

有了上一次經驗,直覺告訴我我還得再測一測,搞不好還有其它 BUG ... 這次找了丁丁來考試,丁丁果真是個人才,交了一份全都錯的答案卷給我,前兩題放棄,第三題全選錯 (PAPER-NATIVE.xml):

丁丁的答案卷: 倒扣 (PAPER-NATIVE.xml)[copy code]
<?xml version="1.0" encoding="utf-8" ?><quiz>	<question>		<item checked="false" />		<item checked="false" />		<item checked="false" />		<item checked="false" />	</question>	<question>		<item checked="false" />		<item checked="false" />		<item checked="false" />		<item checked="false" />	</question>	<question>		<item checked="true" />		<item checked="false" />		<item checked="false" />		<item checked="true" />	</question></quiz>
   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <quiz>
   3:    <question>
   4:      <item checked="false" />
   5:      <item checked="false" />
   6:      <item checked="false" />
   7:      <item checked="false" />
   8:    </question>
   9:    <question>
  10:      <item checked="false" />
  11:      <item checked="false" />
  12:      <item checked="false" />
  13:      <item checked="false" />
  14:    </question>
  15:    <question>
  16:      <item checked="true" />
  17:      <item checked="false" />
  18:      <item checked="false" />
  19:      <item checked="true" />
  20:    </question>
  21:  </quiz>

 

果然有柯南的地方就有密室殺人事件... @_@,又被我抓到一個問題。這次得到的總分是 -40,那有人扣到負的? 工程師又被我叫來唸了一頓,這次改了這版程式給我 (第十一行,最低是0分):

 

修正後的程式 #3: 倒扣到0分為止[copy code]
        public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)        {            int questionCount = quizDoc.SelectNodes("/quiz/question").Count;            int totalScore = 0;            for (int questionPos = 0; questionPos < questionCount; questionPos++)            {                XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;                XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;                totalScore += ComputeQuestionScore(quiz_question, paper_question);            }            return Math.Max(0, totalScore);        }
   1:  public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
   2:  {
   3:      int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
   4:      int totalScore = 0;
   5:      for (int questionPos = 0; questionPos < questionCount; questionPos++)
   6:      {
   7:          XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   8:          XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
   9:          totalScore += ComputeQuestionScore(quiz_question, paper_question);
  10:      }
  11:      return Math.Max(0, totalScore);
  12:  }

 

金融業最重視的就是錢了,銀行的程式連一毛錢都不能算錯,而在線上考試的系統也一樣,連一分都不能算錯。只是當你的老闆這樣要求你的時後,你是謹記在心,還是照一般方式寫程式嗎? 還是你有什麼有效的措施可以預防這些問題? 這時才是顯示你專業的地方啊... 套句鄉民的慣用語:

 

"閃開! 讓專業的來..."

 

哈哈,來看看鄉民... 不,專家該怎麼解決這種問題。怕程式錯就加上一堆檢查就好了。上面舉的例子真的只是 BUG 而以,其它還有更多不可預測的問題,像是題目跟答案卷跟本搭不起來,或是沒有答案卷等等鳥問題都有可能發生。那怎辦? 可憐的工程師被我訓了一頓,只好摸摸鼻子加了一堆令人哭笑不得的 check code, 像這樣:

多了一堆 CHECK 及印出 DEBUG MESSAGE 的程式碼[copy code]
        public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)        {            int totalScore = 0;            int itemCount = quiz_question.SelectNodes("item").Count;            if (quiz_question == null)            {                throw new Exception("沒有題目卷");            }            if (paper_question == null)            {                throw new Exception("沒有答案卷");            }            //            //  如果都沒作答, 此題放棄            //            if (paper_question.SelectNodes("item[@checked='true']").Count == 0)            {                Console.WriteLine("偵測到沒作答的答案,此題放棄");                return 0;            }            //            //  確認題目跟答案的選項數目一致            //            if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)            {                throw new Exception("此題的選項跟題目定義不符合");            }
   1:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
   2:  {
   3:      int totalScore = 0;
   4:      int itemCount = quiz_question.SelectNodes("item").Count;
   5:      if (quiz_question == null)
   6:      {
   7:          throw new Exception("沒有題目卷");
   8:      }
   9:      if (paper_question == null)
  10:      {
  11:          throw new Exception("沒有答案卷");
  12:      }
  13:      //
  14:      //  如果都沒作答, 此題放棄
  15:      //
  16:      if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
  17:      {
  18:          Console.WriteLine("偵測到沒作答的答案,此題放棄");
  19:          return 0;
  20:      }
  21:      //
  22:      //  確認題目跟答案的選項數目一致
  23:      //
  24:      if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)
  25:      {
  26:          throw new Exception("此題的選項跟題目定義不符合");
  27:      }

 

老實說這範例我也寫不下去了,加這麼多 check 是好事,不過事情都有黑暗面,我覺的不妥的地方有幾個:

  1. 可讀性變差
    太多的 check / debug code, 完全把正常流程的 code 淹沒了,一眼看去看不出什麼邏輯...
  2. 效能變差
    對我來說,有些問題是輸入造成的 (如沒有給答案卷),有些是鳥程式自己沒寫好 (如前面的例子)。並不是所有的 check 都需要寫在程式裡。
  3. 花在寫 check 程式的時間太多
    沒錯,寫個程式五分鐘就搞定,寫 check 要多花廿分鐘...

即使這樣,我還是贊成要這樣做。只是要做的聰明一點,要消掉上面的疑慮,還要達成一樣的效果。不需要什麼新技術,十幾年前馬奎爾這本 "Write Solid Code" 就講的很清楚了,要同時維護 RELEASE / DEBUG 兩種版本的程式!

在 C 的年代,只靠兩個巨集就解決了,分別是 TRACE 跟 ASSERT。一個就相當於 printf,可以印出 MESSAGE,另一個 ASSERT 則什麼都不做,只要你傳給它當參數是 TRUE 的話。否則就會印出錯誤訊息同時中止程式。而這兩個巨集都有個特點,就是只在 DEBUG MODE 發生作用,如果是在 RELEASE MODE,則一點用都沒有,就像你沒寫這段 CODE 一樣。

細節我就不多說了,這本書講的很清楚,我直接來用。老實說這種應用太經典了,經典到每種程式語言跟開發工具都有支援,連 Microsoft 在 JavaScript 都有實作,甚至跟 debugger 也有整合,不過不曉得有多少人知道? 在 .NET 當然也有 (System.Diagnoistics)。來看看我改版過的 code:

套用 TRACE / ASSERT 的程式碼[copy code]
        public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)        {            Trace.Assert(quizDoc != null);            Trace.Assert(paperDoc != null);            Trace.Assert(quizDoc.SelectNodes("/quiz/question").Count == paperDoc.SelectNodes("/quiz/question").Count);            int questionCount = quizDoc.SelectNodes("/quiz/question").Count;            int totalScore = 0;            for (int questionPos = 0; questionPos < questionCount; questionPos++)            {                XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;                XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;                totalScore += ComputeQuestionScore(quiz_question, paper_question);            }            totalScore = Math.Max(0, totalScore);            Trace.Assert(totalScore >= 0);            return totalScore;        }        public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)        {            int totalScore = 0;            int itemCount = quiz_question.SelectNodes("item").Count;            //if (quiz_question == null)            //{            //    throw new Exception("沒有題目卷");            //}            //if (paper_question == null)            //{            //    throw new Exception("沒有答案卷");            //}            ////            ////  確認題目跟答案的選項數目一致            ////            //if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)            //{            //    throw new Exception("此題的選項跟題目定義不符合");            //}            Trace.Assert(quiz_question != null);            Trace.Assert(paper_question != null);            Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);            //            //  如果都沒作答, 此題放棄            //            if (paper_question.SelectNodes("item[@checked='true']").Count == 0)            {                //Console.WriteLine("偵測到沒作答的答案,此題放棄");                Trace.WriteLine("偵測到沒作答的答案,此題放棄");                return 0;            }            //            //  題目的配分            //            int quiz_score = int.Parse(quiz_question.GetAttribute("score"));            //            //  答對一個選項的分數            //            int item_score = quiz_score / itemCount;            for (int itemPos = 0; itemPos < itemCount; itemPos++)            {                XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;                XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;                //                //  算成積                //                if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))                {                    totalScore += item_score;                }                else                {                    totalScore -= item_score;                }            }            Trace.Assert(totalScore >= (0 - quiz_score));            Trace.Assert(totalScore <= quiz_score);                        return totalScore;        }
   1:  public static int ComputeQuizScore(XmlDocument quizDoc, XmlDocument paperDoc)
   2:  {
   3:      Trace.Assert(quizDoc != null);
   4:      Trace.Assert(paperDoc != null);
   5:      Trace.Assert(quizDoc.SelectNodes("/quiz/question").Count == paperDoc.SelectNodes("/quiz/question").Count);
   6:      int questionCount = quizDoc.SelectNodes("/quiz/question").Count;
   7:      int totalScore = 0;
   8:      for (int questionPos = 0; questionPos < questionCount; questionPos++)
   9:      {
  10:          XmlElement quiz_question = quizDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
  11:          XmlElement paper_question = paperDoc.SelectNodes("/quiz/question")[questionPos] as XmlElement;
  12:          totalScore += ComputeQuestionScore(quiz_question, paper_question);
  13:      }
  14:      totalScore = Math.Max(0, totalScore);
  15:      Trace.Assert(totalScore >= 0);
  16:      return totalScore;
  17:  }
  18:  public static int ComputeQuestionScore(XmlElement quiz_question, XmlElement paper_question)
  19:  {
  20:      int totalScore = 0;
  21:      int itemCount = quiz_question.SelectNodes("item").Count;
  22:      //if (quiz_question == null)
  23:      //{
  24:      //    throw new Exception("沒有題目卷");
  25:      //}
  26:      //if (paper_question == null)
  27:      //{
  28:      //    throw new Exception("沒有答案卷");
  29:      //}
  30:      ////
  31:      ////  確認題目跟答案的選項數目一致
  32:      ////
  33:      //if (paper_question.SelectNodes("item").Count != quiz_question.SelectNodes("item").Count)
  34:      //{
  35:      //    throw new Exception("此題的選項跟題目定義不符合");
  36:      //}
  37:      Trace.Assert(quiz_question != null);
  38:      Trace.Assert(paper_question != null);
  39:      Trace.Assert(paper_question.SelectNodes("item").Count == quiz_question.SelectNodes("item").Count);
  40:      //
  41:      //  如果都沒作答, 此題放棄
  42:      //
  43:      if (paper_question.SelectNodes("item[@checked='true']").Count == 0)
  44:      {
  45:          //Console.WriteLine("偵測到沒作答的答案,此題放棄");
  46:          Trace.WriteLine("偵測到沒作答的答案,此題放棄");
  47:          return 0;
  48:      }
  49:      //
  50:      //  題目的配分
  51:      //
  52:      int quiz_score = int.Parse(quiz_question.GetAttribute("score"));
  53:      //
  54:      //  答對一個選項的分數
  55:      //
  56:      int item_score = quiz_score / itemCount;
  57:      for (int itemPos = 0; itemPos < itemCount; itemPos++)
  58:      {
  59:          XmlElement quiz_item = quiz_question.SelectNodes("item")[itemPos] as XmlElement;
  60:          XmlElement paper_item = paper_question.SelectNodes("item")[itemPos] as XmlElement;
  61:          //
  62:          //  算成積
  63:          //
  64:          if (quiz_item.GetAttribute("correct") == paper_item.GetAttribute("checked"))
  65:          {
  66:              totalScore += item_score;
  67:          }
  68:          else
  69:          {
  70:              totalScore -= item_score;
  71:          }
  72:      }
  73:      Trace.Assert(totalScore >= (0 - quiz_score));
  74:      Trace.Assert(totalScore <= quiz_score);
  75:      return totalScore;
  76:  }

 

我特地把之前加的亂七八糟的 check code 用註解留下來,各位可以看看用 TRACE / ASSERT 前後的差別有多少。ASSERT是其中的精華。你可以到處都加上 ASSERT ,來說明你對於程式執行到某個地方的 "假設"。舉例來說,你 "假設" 呼叫你 FUNC 的人一定會傳 quizDoc 跟 paperDoc 給你,你又不想為了它寫一堆 IF ....,你就可以簡單的加上這一行 ASSERT( quizDoc != null), 代表只有 quizDoc 不是 NULL 時才是 "正常" 的。

那真的不正常的話會怎樣? 我特地拿掉倒扣扣到 0 分為止的 check, 用新版的 code 執行看看。

image

在 .NET 裡 ASSERT 觸動後就是這個樣子。那 TRACE 呢? 我們進 DEBUG MODE 來看看:

image

TRACE Message 直接被收到 Visual Studio 的 Output 視窗內。不過在 .NET 環境下,這兩者的行為已經跟書上講的廿年前作法有很多不同了。這些機制仍然可以開關,不過已經不是靠 DEBUG / RELEASE MODE 來切換,而是在 .NET configuration 裡用設定檔的方式來切換。

 

 

 

------------------------------------

果然寫到一半寫不完 @_@,先做個小結。這些技巧都是一般人寫程式不會注意的,然而這些才是你寫的程式品質有沒有比別人好的關鍵,要讓你的程式可靠,做好預防措施是很重要的。你沒有辦法在所有地方都派警衛防守,但是你至少可以張貼警告標示,ASSERT 就是這樣的東西。下一篇會更進一步的以這例子為延申,ASSERT 還有更強大的應用。也許有人看到這裡會想說:

"怎麼跟單元測試有點像? 我們直接用 UnitTest 就好了啊"

沒錯,單元測試其實就是從最基本的 Trace / Assert 衍生出來的,一直到這幾年才成為顯學。後續幾篇也會再對這些議題做討論,敬請期待 :D



10/18/2008 11:53:00 AM

生產者 vs 消費者 - BlockQueue 實作

543 | C# | Threading | 作業系統 | 我的作品 | 技術隨筆 | 物件導向 | Microsoft.NET | [精選文章] Facebook Share

過去寫了好幾篇跟執行緒相關的文章,講的都是如何精確控制執行緒的問題。不過實際上有在寫的人就知道,那些只是 "工具",最重要的還是你該怎樣安排你的程式,讓它能有效率的用到執行緒的好處,那才是重點。大部份能有效利用到多執行緒的程式,大都是大量且獨立的小動作,可以很簡單的撒下去給ThreadPool處理,不過當你的程式沒辦法這樣切,就要想點別的辦法了。

 

開始看 code 前先講講簡單的概念。這篇要講的是另一種模式: "生產者 v.s. 消費者"。這是個很典型的供需問題,唸過作業系統 (Operation System) 的人應該都被考過這個課題吧 @_@。簡單的說如果你的程式要處理的動作可以分為 "生產者" (產生資料,載入檔案,或是第一階段的運算等等) 及消費者 (匯出資料,或是第二階段的運算等等) 這種模式,而前後兩個階段各自又適合用執行緒來加速的話,那你就值得來研究一下這種模式。第一手資料就是去看看作業系統的書,恐龍書足足有一整章在講,足夠你研究了。本篇重點會擺在怎樣用 C# / .NET 實作的部份。

 

舉個具體一點的例子,如果你想寫個程式,從網站下載幾百個檔案,同時要把它們壓縮成一個 ZIP 檔,在過去你大概只能全部下載完之後,再開始ZIP的壓縮動作。第一階段都是 IO (網路) bound 的程式,第二階段則是 CPU bound。如果是完全獨立的兩個程式,很適合擺在一起執行,因為它們需要的資源不一樣,不會搶來搶去。但是就敗在他們要處理的資料是卡在一起的。

 

把這個動作想像成我們有兩組人分別負責下載及壓縮的動作,下載的部份可以多執行緒同時進行沒問題,但是下載好一個檔案,就可以先丟給後面的那組人開始壓縮了,不用等期它人下載完成。如果下載的暫存目錄空間有限,我們甚至可以這樣調整: 當 TEMP 滿了的話,下載動作就暫停,等到 TEMP 裡的東西壓縮好清掉一部份後再繼續。而壓縮的部份則相反,如果 TEMP 已經空了就暫停,等到有東西進來再繼續,直到完成為止。

 

image

前後兩階段該如何利用多執行緒,我就跳過去了,過去那幾篇就足以應付。這種模式的關鍵在於前後兩階段的進度該如何平衡。有些範例是有照規矩的把這模式實作出來,不過... 你也知道,看起來就是像作業的那種,完全不像是可以拿來正規的用途。

 

我認定 "好" 的實作,是像 System.Collections.Generics 之於 DataStructure 那樣,能很漂亮的把細節封裝起來,很容易重複利用的才是我認為好的實作。不能容易的使用,那就只能像作業一樣寫完就丟...。這個問題看過有人用 Semaphore 來做,也是作的很棒,不過我比較推薦的是 QUEUE 的作法。

 

從上圖來看,生產者跟消費者都很簡單,就是過去講的多執行緒或是執行緒集區就搞定,關鍵在於中間的控制。我的想法是把庫存管理的東西實作成佇列 (QUEUE),生產者產出的東西就放到 QUEUE,而消費者就去 QUEUE 把東西拿出來。不過現成的 QUEUE 不會告訴你 QUEUE 滿了,QUEUE 空了也只會丟 EXCEPTION 而以。這次我做了個 BlockQueue 就是希望解決這個問題。

 

我期望這個 QUEUE 能跟一般的 QUEUE 一樣使用,但是要有三個地方不一樣:

  1. 要設定大小限制,當 QUEUE 達到容量上限時 EnQueue 的動作會被暫停 (Block),而不是丟出例外。
  2. QUEUE 已經空了的時後,DeQueue 的動作會被暫停 (Block),而不是丟出例外。
  3. 要多個 QUEUE 關機的動作 (SHUTDOWN),以免生產者都不出貨了,消費者還苦苦的等下去的窘況。

 

先看看這樣的 QUEUE 我希望它怎麼被使用。看一下簡單的範例程式 (主程式,不包含 BlockQueue):

使用 BlockQueue 來實作的生產者/消費者範例[copy code]
        public static BlockQueue<string> queue = new BlockQueue<string>(10);        public static void Main(string[] args)        {            List<Thread> ps = new List<Thread>();            List<Thread> cs = new List<Thread>();            for (int index = 0; index < 5; index++)            {                Thread t = new Thread(Producer);                ps.Add(t);                t.Start();            }            for (int index = 0; index < 10; index++)            {                Thread t = new Thread(Consumer);                cs.Add(t);                t.Start();            }            foreach (Thread t in ps)            {                t.Join();            }            WriteLine("Producer shutdown.");            queue.Shutdown();            foreach (Thread t in cs)            {                t.Join();            }        }        public static long sn = 0;        public static void Producer()        {            for (int count = 0; count < 30; count++)            {                RandomWait();                string item = string.Format("item:{0}", Interlocked.Increment(ref sn));                WriteLine("Produce Item: {0}", item);                queue.EnQueue(item);            }            WriteLine("Producer Exit");        }        public static void Consumer()        {            try            {                while (true)                {                    RandomWait();                    string item = queue.DeQueue();                    WriteLine("Cust Item: {0}", item);                }            }            catch            {                WriteLine("Consumer Exit");            }        }        private static void RandomWait()        {            Random rnd = new Random();            Thread.Sleep((int)(rnd.NextDouble() * 300));        }        private static void WriteLine(string patterns, params object[] arguments)        {            Console.WriteLine(string.Format("[#{0:D02}] ", Thread.CurrentThread.ManagedThreadId) + patterns, arguments);        }
   1:  public static BlockQueue<string> queue = new BlockQueue<string>(10);
   2:  public static void Main(string[] args)
   3:  {
   4:      List<Thread> ps = new List<Thread>();
   5:      List<Thread> cs = new List<Thread>();
   6:      for (int index = 0; index < 5; index++)
   7:      {
   8:          Thread t = new Thread(Producer);
   9:          ps.Add(t);
  10:          t.Start();
  11:      }
  12:      for (int index = 0; index < 10; index++)
  13:      {
  14:          Thread t = new Thread(Consumer);
  15:          cs.Add(t);
  16:          t.Start();
  17:      }
  18:      foreach (Thread t in ps)
  19:      {
  20:          t.Join();
  21:      }
  22:      WriteLine("Producer shutdown.");
  23:      queue.Shutdown();
  24:      foreach (Thread t in cs)
  25:      {
  26:          t.Join();
  27:      }
  28:  }
  29:  public static long sn = 0;
  30:  public static void Producer()
  31:  {
  32:      for (int count = 0; count < 30; count++)
  33:      {
  34:          RandomWait();
  35:          string item = string.Format("item:{0}", Interlocked.Increment(ref sn));
  36:          WriteLine("Produce Item: {0}", item);
  37:          queue.EnQueue(item);
  38:      }
  39:      WriteLine("Producer Exit");
  40:  }
  41:  public static void Consumer()
  42:  {
  43:      try
  44:      {
  45:          while (true)
  46:          {
  47:              RandomWait();
  48:              string item = queue.DeQueue();
  49:              WriteLine("Cust Item: {0}", item);
  50:          }
  51:      }
  52:      catch
  53:      {
  54:          WriteLine("Consumer Exit");
  55:      }
  56:  }
  57:  private static void RandomWait()
  58:  {
  59:      Random rnd = new Random();
  60:      Thread.Sleep((int)(rnd.NextDouble() * 300));
  61:  }
  62:  private static void WriteLine(string patterns, params object[] arguments)
  63:  {
  64:      Console.WriteLine(string.Format("[#{0:D02}] ", Thread.CurrentThread.ManagedThreadId) + patterns, arguments);
  65:  }

 

 

 

 

 

主程式很簡單,你知道怎麼寫多執行緒程式的話那麼一看就懂了。一開始替 Producer / Consumer 各建立三個執行續,而每個 Producer 只作很簡單的事,就是連續生產 30 個字串放到 BlockQueue, 當所有的 Producer thread 都執行完後,會呼叫 queue.Shutdown( ); 通知 QUEUE 已經全部生產完畢。

Consumer 也很簡單,每個 Consumer 只是去 Queue 拿東西出來,顯示在 Console 上。直到 Dequeue 動作失敗,接到 Exception 之後就結束。

要試試生產者/消費者模式的各種狀況,可以試著調整兩者的執行緒數量。舉例來說,調大 Producer 執行緒數量時 (P: 10 / C:5),結果是這樣:

image

Producer 的進度大約就是領先 Consumer 的進度 10 筆資料左右,領先的幅度就暫停了,不會無止境的成長下去。證明卡在 QUEUE 內的數量受到控制。接下來再來看看調高 Consumer 的執行緒數量的結果:

image

好像 iPhone 上市搶購熱潮一樣 @_@,供不應求,Producer 提供的資料馬上被搶走了...。

 

效果不錯,看來這樣的實作有達成它的目的。最後來看的是 BlockQueue 的程式碼:

 

BlockQueue<T> 實作的完整程式碼[copy code]
    public class BlockQueue<T>    {        public readonly int SizeLimit = 0;        private Queue<T> _inner_queue = null;        private ManualResetEvent _enqueue_wait = null;        private ManualResetEvent _dequeue_wait = null;        public BlockQueue(int sizeLimit)        {            this.SizeLimit = sizeLimit;            this._inner_queue = new Queue<T>(this.SizeLimit);            this._enqueue_wait = new ManualResetEvent(false);            this._dequeue_wait = new ManualResetEvent(false);        }        public void EnQueue(T item)        {            if (this._IsShutdown == true) throw new InvalidCastException("Queue was shutdown. Enqueue was not allowed.");            while (true)            {                lock (this._inner_queue)                {                    if (this._inner_queue.Count < this.SizeLimit)                    {                        this._inner_queue.Enqueue(item);                        this._enqueue_wait.Reset();                        this._dequeue_wait.Set();                        break;                    }                }                this._enqueue_wait.WaitOne();            }        }        public T DeQueue()        {            while (true)            {                if (this._IsShutdown == true)                {                    lock (this._inner_queue) return this._inner_queue.Dequeue();                }                lock (this._inner_queue)                {                    if (this._inner_queue.Count > 0)                    {                        T item = this._inner_queue.Dequeue();                        this._dequeue_wait.Reset();                        this._enqueue_wait.Set();                        return item;                    }                }                this._dequeue_wait.WaitOne();            }        }        private bool _IsShutdown = false;        public void Shutdown()        {            this._IsShutdown = true;            this._dequeue_wait.Set();        }    }
   1:  public class BlockQueue<T>
   2:  {
   3:      public readonly int SizeLimit = 0;
   4:      private Queue<T> _inner_queue = null;
   5:      private ManualResetEvent _enqueue_wait = null;
   6:      private ManualResetEvent _dequeue_wait = null;
   7:      public BlockQueue(int sizeLimit)
   8:      {
   9:          this.SizeLimit = sizeLimit;
  10:          this._inner_queue = new Queue<T>(this.SizeLimit);
  11:          this._enqueue_wait = new ManualResetEvent(false);
  12:          this._dequeue_wait = new ManualResetEvent(false);
  13:      }
  14:      public void EnQueue(T item)
  15:      {
  16:          if (this._IsShutdown == true) throw new InvalidCastException("Queue was shutdown. Enqueue was not allowed.");
  17:          while (true)
  18:          {
  19:              lock (this._inner_queue)
  20:              {
  21:                  if (this._inner_queue.Count < this.SizeLimit)
  22:                  {
  23:                      this._inner_queue.Enqueue(item);
  24:                      this._enqueue_wait.Reset();
  25:                      this._dequeue_wait.Set();
  26:                      break;
  27:                  }
  28:              }
  29:              this._enqueue_wait.WaitOne();
  30:          }
  31:      }
  32:      public T DeQueue()
  33:      {
  34:          while (true)
  35:          {
  36:              if (this._IsShutdown == true)
  37:              {
  38:                  lock (this._inner_queue) return this._inner_queue.Dequeue();
  39:              }
  40:              lock (this._inner_queue)
  41:              {
  42:                  if (this._inner_queue.Count > 0)
  43:                  {
  44:                      T item = this._inner_queue.Dequeue();
  45:                      this._dequeue_wait.Reset();
  46:                      this._enqueue_wait.Set();
  47:                      return item;
  48:                  }
  49:              }
  50:              this._dequeue_wait.WaitOne();
  51:          }
  52:      }
  53:      private bool _IsShutdown = false;
  54:      public void Shutdown()
  55:      {
  56:          this._IsShutdown = true;
  57:          this._dequeue_wait.Set();
  58:      }
  59:  }

 

重點只在重新包裝 Queue 的 Enqueue / Dequeue ,及追加的 Shutdown( ) 裡做的執行緒同步機制。在 BlockQueue 尚未 Shutdown 之前,Enqueue / Dequeue 都不會引發 Exception, 取代的是用 ManualResetEvent 的 WaitOne( ) 來暫停這個動作,直到另一端資料準備好為止。

然而當 Shutdown 被呼叫過之後,Queue 就不再接受新的東西被塞進來了,而東西拿光因為不再補貨,所以就維持原本 Queue 的設計扔出 Exception。

 

其實真的要挖的話,這個 Queue 可以進一步的改善,以資料結構來看,這種有固定 SIZE 上限的 QUEUE,最適合用 CircleQueue 來實作了。有興趣的朋友們可以換上回介紹過的 NGenerics 改看看,我就不再示範了。其實還有其它變型,像是 Priority Queue, 進去跟出來的順序不一定一樣,意思是你地位比較高的話是可以 "插隊" 的,後加入 QUEUE 的物件,可以優先被拿出來。這些機制都是可以進一步改善 "生產者/消費者" 模式的方法,有需要的讀者們可以朝這個方向思考看看!

 

這篇只是個開始,運用這種機制,可以進一步延伸出 Pipeline 模式 (生產線),甚至更進一步運用到串流 (Stream) 的應用。運氣好的話下個月應該看的到完整的探討跟解說吧 ...,敬請期待 :D



10/8/2008 3:32:00 AM

該如何學好 "寫程式" #3. 進階應用 - 資料結構 + 問題分析

543 | C# | 小技巧 | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章] Facebook Share

接續前文:

  1. 該如何學好 "寫程式" ??
  2. 該如何學好 "寫程式" #2. 為什麼 programmer 該學資料結構 ??

這類文章還真不好寫,想了好幾天,才擠的出一篇文章。第一篇已經講了一堆教條了,第二篇也舉了簡單的例子,說明挑對資料結構的重要性,接下來這篇會把主題放在更複雜的例子上,到底那些地方該注重技術,而那些地方該把重點放在基礎的資料結構及演算法身上。

這次不囉唆半天了,先來回顧一下第一篇,我出的題目是這樣:

以台灣高速公路為題 (中山高、北二高、國道二號),你有辦法寫程式,讓使用者指定起點跟終點的交流道,然後替它找出建議的路線嗎? (把延路經過的交流到跟收費站列出來就好)。

舉這個題目,是怕前面的例子被嫌太簡單,一點都不能符合實際的情況。沒錯,絕大部份的情況都不會像上一篇的範例一樣,放一堆資料在記憶體裡 SEARCH 出來就了事那麼簡單。高速公路的問題核心一樣是在資料結構,不過這次多了必需自己實作的演算法。

跟我一樣五六年級的人,都聽過這句話吧,PASCAL 之父 (Niklaus Wirth) 講的這句名言: "程式 = 資料結構+演算法",沒錯,這個範例就需要用到這兩種能力才搞的定。依我的看法,解決這問題有三道關卡要闖:

  1. 你該用什麼樣的方式來儲存這樣的地圖資訊?
    這裡會用到的知識,是資料結構裡的 GRAPH,典型的方法就是分成點跟線來記錄..
  2. 你該用什麼樣的演算法,找出你要的最佳路線?
    最基本的是要找出所有可走的路線 (走迷宮),再找出其中最便宜的一條路。
  3. 你的程式的結構該如何設計?
    這部份跟課本比較無關,講的是你對程式語言及可用的函式庫/工具的掌握,還有架構等等。

這三道關卡,要依序破解,前一關的決定會影響到後面的解決方式。先從資料來說,連該怎麼記錄這些資料,就別想解題了。最基本的 GRAPH 需要點 (NODE) 及點跟點之間的連線 (LINK) 組成。很直覺的就可以定出這兩個類別:

描述交流道/收費站的 class[copy code]
    public class Node    {        public string Name = null;        public int TollFee = 0;        public List<Link> Links = new List<Link>();        public Node(string name, int tollFee)        {            this.Name = name;            this.TollFee = tollFee;        }    }
   1:  public class Node
   2:  {
   3:      public string Name = null;
   4:      public int TollFee = 0;
   5:      public List<Link> Links = new List<Link>();
   6:      public Node(string name, int tollFee)
   7:      {
   8:          this.Name = name;
   9:          this.TollFee = tollFee;
  10:      }
  11:  }

 

描述兩個點之間的路段 (Link) 的 class[copy code]
    public class Link    {        public double Distance = 0D;        public Node FromNode = null;        public Node ToNode = null;        public RoadNameEnum Road;        public Link(Node from, Node to, double distance, RoadNameEnum road)        {            this.FromNode = from;            this.ToNode = to;            this.Distance = distance;            this.Road = road;        }        public enum RoadNameEnum        {            Highway1,            Highway2,            Highway3        }    }
   1:  public class Link
   2:  {
   3:      public double Distance = 0D;
   4:      public Node FromNode = null;
   5:      public Node ToNode = null;
   6:      public RoadNameEnum Road;
   7:      public Link(Node from, Node to, double distance, RoadNameEnum road)
   8:      {
   9:          this.FromNode = from;
  10:          this.ToNode = to;
  11:          this.Distance = distance;
  12:          this.Road = road;
  13:      }
  14:      public enum RoadNameEnum
  15:      {
  16:          Highway1,
  17:          Highway2,
  18:          Highway3
  19:      }
  20:  }

好像沒什麼特別的,每個點除了搭配 List<Link> 來記錄所有經過它的路段 (Node.Links) 之外,也標上了這個點的名字 (Node.Name),跟過路費 (Node.TollFee)。而每個路段則記錄了它兩個端點 (Link.FromNode, Link.ToNode) 之外,也額外記錄了路段的距離 (Node.Distance),及它是屬於那一條高速公路的資訊 (Link.Road)。

接下來就要載入資料了。我偷個懶,只記中山高跟北二高新竹以北的部份,還有機場國道。實在是沒力氣把全部的路段打完... 哈哈。資料來源是參考國道高速公路局的網頁。以下是 Map 類別的部份程式碼,及載入部份地圖資訊的程式碼:

MAP[copy code]
    public class Map    {        private Dictionary<string, Node> _nodes = new Dictionary<string, Node>();        public Map()        {            //            //  construct / load map data            //            this.AddNode("基金", 0);            this.AddNode("七堵收費站", 40);            this.AddNode("汐止系統", 0);            this.AddNode("樹林收費站", 40);            // 略            this.AddLink("基金", "七堵收費站", 4.9-0, Link.RoadNameEnum.Highway3);            this.AddLink("七堵收費站", "汐止系統", 10.9-4.9, Link.RoadNameEnum.Highway3);            // 略        }        private void AddNode(string name, int tollFee)        {            Node n = new Node(name, tollFee);            this._nodes.Add(name, n);        }        private void AddLink(string n1, string n2, double distance, Link.RoadNameEnum road)        {            Node node1 = this._nodes[n1];            Node node2 = this._nodes[n2];            Link link = new Link(this._nodes[n1], this._nodes[n2], distance, road);            node1.Links.Add(link);            node2.Links.Add(link);        }        public Link FindLink(string name1, string name2)        {            foreach (Link way in this._nodes[name1].Links)            {                if (way.GetOtherNodeName(name1) == name2) return way;            }            return null;        }    }
   1:  public class Map
   2:  {
   3:      private Dictionary<string, Node> _nodes = new Dictionary<string, Node>();
   4:      public Map()
   5:      {
   6:          //
   7:          //  construct / load map data
   8:          //
   9:          this.AddNode("基金", 0);
  10:          this.AddNode("七堵收費站", 40);
  11:          this.AddNode("汐止系統", 0);
  12:          this.AddNode("樹林收費站", 40);
  13:          // 略
  14:          this.AddLink("基金", "七堵收費站", 4.9-0, Link.RoadNameEnum.Highway3);
  15:          this.AddLink("七堵收費站", "汐止系統", 10.9-4.9, Link.RoadNameEnum.Highway3);
  16:          // 略
  17:      }
  18:      private void AddNode(string name, int tollFee)
  19:      {
  20:          Node n = new Node(name, tollFee);
  21:          this._nodes.Add(name, n);
  22:      }
  23:      private void AddLink(string n1, string n2, double distance, Link.RoadNameEnum road)
  24:      {
  25:          Node node1 = this._nodes[n1];
  26:          Node node2 = this._nodes[n2];
  27:          Link link = new Link(this._nodes[n1], this._nodes[n2], distance, road);
  28:          node1.Links.Add(link);
  29:          node2.Links.Add(link);
  30:      }
  31:      public Link FindLink(string name1, string name2)
  32:      {
  33:          foreach (Link way in this._nodes[name1].Links)
  34:          {
  35:              if (way.GetOtherNodeName(name1) == name2) return way;
  36:          }
  37:          return null;
  38:      }
  39:  }

 

第一步準備動作完成了。接下來就是想辦法在 class Map 裡加上 FindBestWay( ) method, 來找出最佳路線。在這邊先定義一下什麼叫最佳路線。一般不外乎是找最短的路線,或是通過最少的收費站,我們來點實際的,以油價每公升 30 元為例,車子就假設一公升可以跑 15 公里好了,因此每跑一公里要花兩塊錢。最佳路逕就是花的油錢跟過路費最少的為準。

沒唸過資料結構的朋友們,現在大概卡住了。該怎樣找出最佳的路逕? 電腦什麼不行,就是計算很快,這種最佳解的問題,通常都可以用暴力法解決。只要把所有的路線找出來,然後找出總花費最便宜的那個路線就好。雖然資料結構的書通常會舉其它更有效率的演算法,其中一個演算法的名字我不記得了,大致的步驟是由起點開始往外擴散,先算走一步可以走到那些點,再往外推,如果到同一點有兩條以上的路線,就保留便宜的那個... 直到推到終點為止。

不過這方法寫起來比較麻煩,我挑另一個簡單一點的 (相對的比較沒效率),就是搭配 STACK 走迷宮的方法,把所有路線試過一次,找出所有能從起點到終點的路線,再從其中挑出最經濟的。

為什麼我會挑這個? 只是因為它的邏輯比較簡單易懂,畢竟這個程式不是在比賽,要去拼最快的話就不用了.. 哈哈。資料結構在講到 TREE 一定會講到怎麼樣把 TREE 的每個節點都走一次的方法,就是要搭配 STACK,把走過的點都 PUSH 進去,當作麵包屑來用,等走到沒路了就 POP 退回上一步,改走第二個分岔,直到所有的點都走完為止。

接下來就要把 GRAPH 切掉幾條線,把它想像成長的不大整齊的 TREE,就從起點開始走下去。因為 GRAPH 不像 TREE,有可能會走回原點,因此我們在走的過程中需要跳過已經走過的點,免的最後都在兜圈子繞不出去。

這邊我搭配了遞迴 (RECURSIVE) 的方式來簡化問題。其實就邏輯來說,遞迴幾乎可以跟 STACK 劃上等號。因為遞迴的過程中也是有 STACK 在輔助 (就是 CALL STACK)。不過我偏愛 RECURSIVE,因為藉著 CALL STACK 加上 FUNCTION CALL 傳遞的參數,可以自動幫我處理不少 push / pop, 及替每個階段保存暫時的資料,程式看起來會簡單很多。

找出最經濟路線的程式碼[copy code]
        private double _cost = 0;        private string[] _best_path = null;        private Stack<string> _path = null;        private void Search(string startName, string endName, double current_cost)        {            this._path.Push(startName);            if (startName == endName)            {                if (this._cost == 0 || current_cost < this._cost)                {                    this._cost = current_cost;                    this._best_path = this._path.ToArray();                }                this._path.Pop();                return;            }            foreach (Link way in this._nodes[startName].Links)            {                string next = way.GetOtherNodeName(startName);                if (this._path.Contains(next) == false)                {                    this.Search(                        next,                        endName,                        current_cost + this._nodes[next].TollFee + way.Distance * 3);                }            }            this._path.Pop();        }        public string[] FindBestPath(string startName, string endName, out double cost)        {            try            {                this._cost = 0;                this._path = new Stack<string>();                this.Search(startName, endName, 0);                cost = this._cost;                return this._best_path;            }            finally            {                this._cost = 0;                this._path = null;            }        }
   1:  private double _cost = 0;
   2:  private string[] _best_path = null;
   3:  private Stack<string> _path = null;
   4:  private void Search(string startName, string endName, double current_cost)
   5:  {
   6:      this._path.Push(startName);
   7:      if (startName == endName)
   8:      {
   9:          if (this._cost == 0 || current_cost < this._cost)
  10:          {
  11:              this._cost = current_cost;
  12:              this._best_path = this._path.ToArray();
  13:          }
  14:          this._path.Pop();
  15:          return;
  16:      }
  17:      foreach (Link way in this._nodes[startName].Links)
  18:      {
  19:          string next = way.GetOtherNodeName(startName);
  20:          if (this._path.Contains(next) == false)
  21:          {
  22:              this.Search(
  23:                  next,
  24:                  endName,
  25:                  current_cost + this._nodes[next].TollFee + way.Distance * 3);
  26:          }
  27:      }
  28:      this._path.Pop();
  29:  }
  30:  public string[] FindBestPath(string startName, string endName, out double cost)
  31:  {
  32:      try
  33:      {
  34:          this._cost = 0;
  35:          this._path = new Stack<string>();
  36:          this.Search(startName, endName, 0);
  37:          cost = this._cost;
  38:          return this._best_path;
  39:      }
  40:      finally
  41:      {
  42:          this._cost = 0;
  43:          this._path = null;
  44:      }
  45:  }

 

先來看看結果。主程式是要找出 "機場端" 跟 "基金" 交流道之間的最經濟路線,看看程式跑出來的結果:

image

不相信的人就拿紙筆畫一畫算一算吧! 應該是沒算錯啦。這個例子我就不像上一個例子,放上千萬個節點來拼拼看速度到底多快了,因為我沒有現成的資料啊,這東西要產生假資料也麻煩的多,就略過這個步驟了。不過我們倒是可以回過頭來看看,目前這段程式有什麼可以改進的?

首先,在資料數量遽增的情況下,演算法的改善一定是第一要務。你會發現程式碼從五行變成三行,或是從 100ms 進步到 90ms, 這種程度的改善相較之下都是微不足道的,一來這種改善程度通常是固定的,因為演算法沒有變,整體來說可能只是從 100sec 進步到 90sec,我是客戶的話,還不如換顆快一點的 CPU 就好了...。但是演算法的改進,則是讓你迴圈的次數變少,或是比較的次數變少等等,改變幅度通常是以倍數來算,隨便就提升好幾倍的效能。這就不是升級 CPU 可以解決的問題...。還記得上個例子嗎? 從 List 換成 SortedList, 搜尋速度差了 6000 倍... 你要花多少錢才買的到運算速度快 6000 倍的電腦?

除了演算法之外,程式也是有其它地方可以改善的。看到第 20 行程式碼了嗎? 就是找出下一步是不是已經走過了的程式碼:

if (this._path.Contains(next) == false)

其中 _path 是 Stack<string> 物件,養成好習慣,順手查查它的時間複雜度吧,在 MSDN 裡是這麼寫的:

http://msdn.microsoft.com/en-us/library/xeaek790.aspx

This method performs a linear search; therefore, this method is an O(n) operation, where n is Count.

看起來它的效果跟 List 一樣,搜尋都很慢,有幾筆就要比對幾次。還記得上一篇提過什麼方法? 如果排序過的資料,要花的時間是 O(log n), 如果是採用 HashTable 結構的,則只要 O(1)... 再把 MSDN 拿出來翻翻看,發現除了 Dictionary<TKey, TValue> 之外,還有另一個更適合的 HashSet (.NET 3.5 only):

http://msdn.microsoft.com/en-us/library/bb359438.aspx

The HashSet<(Of <(T>)>) class provides high performance set operations. A set is a collection that contains no duplicate elements, and whose elements are in no particular order.

The capacity of a HashSet<(Of <(T>)>) object is the number of elements that the object can hold. A HashSet<(Of <(T>)>) object's capacity automatically increases as elements are added to the object.

馬上看一下,新增一筆及找出某一筆需要的時間複雜度:

 

HashSet<T>.Add( T ):
If Count is less than the capacity of the internal array, this method is an O(1) operation. If the HashSet<(Of <(T>)>) object must be resized, this method becomes an O(n) operation, where n is Count.

 

HashSet<T>.Contains( T ):
This method is an O(1) operation.

 

看起來沒什麼好挑的了。把資料塞進去跟找出來的時間都是固定的,當地圖的節點夠多,你要找的目標夠遠,多花一倍的空間另外放一份 HashSet 絕對是值得的。也因為 HashSet 有這樣的特性,因此它特別適合拿來作集合的運算。比如兩堆資料要找出交集 (Intersection),聯集 (Union) 等等都很方便。既然都講了就順手查看看:

HashSet<T>.IntersectWith(Hash<T>):
If the collection represented by the other parameter is a HashSet<(Of <(T>)>) collection with the same equality comparer as the current HashSet<(Of <(T>)>) object, this method is an O(n) operation. Otherwise, this method is an O(n + m) operation, where n is Count and m is the number of elements in other.

 

 

 

 

---------------------------

本系列文章 [該如何學好 "寫程式"] 第一部份就先到這裡。在這裡作個小結。既然第一部份我是在探討要成為一個好的 "programmer" 該具備的能力,我自然是把重點放在怎樣把你拿到的需求,忠實且正確的寫成 code 為主。這時邏輯及觀念,還有資料結構等等基礎的知識是我認為的重點。也許有些讀者很不以為然,我猜想的大概會有這幾個理由:

  • 我不會這些,程式還不一樣寫的好好的?
  • 都什麼年代了,現在講求的是程式架構!
  • 物件導向不是都講求封裝? 幹嘛還要去挖這些?
  • 現在資料都放資料庫了,還學這幹嘛?
  • ...

其實這些論點也沒錯,不過上一篇可以看到,不懂得這些基礎的話,現成的物件給你挑也不見得知道要挑那一個,更慘的是連之間的差別都不曉得。還有,資料結構通常包含兩個層面,一個是怎麼 "描述" 你的資料? 另一個是怎麼去應用你的資料? 以這題為例,如果你都不曉得要把地圖拆成點跟線來記錄,你會知道 TABLE 該怎麼建嗎?

另外,很多資料庫上面效能的問題,也都跟資料結構有關。就跟上一篇該挑那一種 Collection 一樣,資料庫也可以把它當成一個更巨大,功能更多的 Collection 來看待,因此能不能有效的利用它,資料結構也是很重要的觀念之一。

再來講到架構的部份,我覺的這位網友在他的 blog (我不認得他本人,只是常看他文章) 發表的這兩篇文章很不錯:

1. 程式設計的兩個觀點 (1/2)
2. 程式設計的兩個觀點 (2/2)

他這兩篇講的就是兩個極端,一個講求效率跟演算法,另一個則是講求架構跟程式的美感。而這兩者通常不容易兼顧。以我來說我比較偏後者,效能的部份,我會捨棄一些小地方以換來程式碼的可擴充性,可讀性,架構等等。不過我不會放棄的是資料結構跟演算法的正確性,就如同前面寫的例子一樣,程式碼有沒有最佳化,差的是 xx % 的效能,但是演算法跟資料結構的差距,則是好幾倍。我一向認為不會跑就要學飛,遲早會跌下來的,所以才會寫這三篇針對 programmer 的文章。

接下來,就換到 software engineer 了。這個階段就不只是把程式碼 "寫對" 或是 "寫出來" 而以,而是要開始考慮怎樣才 "寫的好" 了。有興趣的讀者們請耐心等待續集 :D

 

--
範例程式下載: Taiwan-Highway.zip



10/1/2008 10:43:22 PM

得獎了 :D~~~

Microsoft.NET | 543 | C# | Threading | 小技巧 | 安德魯的當年勇 | 我的作品 | 技術隨筆 | 物件導向 Facebook Share

 IMG_9142

 

雖然上禮拜就知道了,不過獎品還沒拿到,當然要忍一下再發表... 哈哈!

花了幾個晚上拼了猜數字的程式,運氣不錯,順利拼到冠軍了。除了寫程式,把心得貼到 BLOG 也花了不少時間.. 主要貼的這四篇:

  1. Thread Sync #1. 概念篇 - 如何化被動為主動?
  2. Thread Sync #2. 實作篇 - 互相等待的兩個執行緒
  3. [C#: yield return] #1. How It Work ?
  4. [C# yield return] #2. 另類的應用 - Thread Sync 替代方案

 

蠻有意思的比賽。雖然過去也參加過不少比賽,運氣不錯也騙到一些獎品...,不過這次倒是寫的最起勁,因為其它比賽都是 "廠商" 讚助,不是 Microsoft 就是 Cisco ... 都要想儘辦法把他們的技術發揮出來才能得獎,老實說寫起來跟工作差不多,總是要滿足那些 "市場" 的需求。

這次題目老實說很 "不實用",純粹是比 code 誰寫的又快又好而已,不過還蠻合我胃口的 :D。正好這次碰到誰呼叫誰這種結構上的問題,就是上面四篇文章一直在討論的 GameHost 為主還是 Player 為主的思考方式,解決這問題花的工夫還比較多。想到這兩套解決方式,我覺的收穫是蠻值得的,至少我多學到兩種不同的設計模式。

最後當然要感謝一下主辦人,下班專程騎車過來頒獎... 哈哈,獎品對我還蠻實用的,算是大獎一枚! 正好是我需要的東西,看來可以開始物色新硬碟,還有要準備來更新我的 SERVER 了 :D



10/1/2008 4:09:00 AM

該如何學好 "寫程式" #2. 為什麼 programmer 該學資料結構 ??

543 | C# | MSDN | 小技巧 | 安德魯的當年勇 | 我的作品 | 技術隨筆 | Microsoft.NET | [精選文章] Facebook Share

自從貼了上一篇 [該如何學好 "寫程式"] 一文,原本以為這種老生常談的內容沒什麼人會看,沒想到還有人給我回應.. :D 原來這種文章還是有市場的。接下來這篇,是延續上一篇,來談談要成為合格的 programmer, 我認為應該要俱備的 "內功" 是什麼。上篇我提到,我認知的 programmer,就是要有實作 (CODING) 的能力。要有能力把技術規格 (像是輸入格式,操作介面等等) 具體的寫成可以執行的程式碼。當然是要寫的又快又好,穩定不當機又沒 BUG ...。

 

在這個階段 (programmer),會一些具體的工具或是技術是必要的,但是它絕對不是主角。如何去運用你的工具才是關鍵。我認為 "資料結構" 就是能正確運用你的工具 (程式語言及函式庫) 最重要的知識。我常看到很多會一堆 "先進" 的技術,卻寫出很可笑的 code ... 。這種例子太多了,兩層迴圈擺錯順序,或是某些動作 *不小心* 擺到迴圈內,多花了好幾倍的時間在做冤枉事...。這種例子我通通把它規在基本功夫不好,或是常聽的邏輯觀念不佳。所以在上一篇我會提到,好的 programmer 至少能滿足我講的三個基本要求:

  1. 丟一付洗過的撲克牌給你 (不要多,黑桃1 ~ 13就好),你知道怎麼用 Bubble Sort / Quick Sort 的步驟把它排好嗎? 丟一個陣列,裡面隨便打幾個數字,你能寫程式把它由小到大排好印出來嗎?
  2. 假設記憶體夠大的話,你有辦法把一百萬筆通訊錄資料讀到記憶體內 (用什麼物件都隨你),然後還能用很快的速度找到你要的資料嗎? 不同的搜尋方式,你知道該用什麼樣的方式找才有效率嗎?
  3. 以台灣高速公路為題 (中山高、北二高、國道二號),你有辦法寫程式,讓使用者指定起點跟終點的交流道,然後替它找出建議的路線嗎? (把延路經過的交流到跟收費站列出來就好)

第一個只要你知道排序的方法,剩下的就是你有沒有本事把腦袋的想法寫成 CODE 而以。這個要求大部份的人都能過關,我就不多作解釋了。來看看第二個要求,它考驗的是你該用什麼樣的方式 "SEARCH" ?

我就以 C# 為例來說明這個問題該怎樣思考。以資訊系的 "資料結構" 這門課的角度來思考,你應該要找出個適合的資料結構 (Binary Tree, Heap, Linked List ... etc) 來存放這堆資料。不過資料結構這麼多種,你都要自己做嗎? .NET framework 已經在 System.Collection.Generic 這命名空間內提供了一堆好用的 Collection 給你用了,你該怎麼挑選才好? 課堂上老師不會教你實作的東西,而公司的前輩也不會教你這種基礎的東西,那你該怎麼把這兩者應用在一起?

就先從 (2) 的例子開始吧! 通訊錄最基本的要求,就是儲存的資料要能按照姓名/EMAIL/電話號碼排序。輸入名字後,要能很快的找到這個人完整的通訊錄。如果能像手機一樣,邊輸入名字就邊過濾名單,直到名字打完人就找到的話更好。在宣告了 class ContactData { ... } 類別來處理一筆資料後,下一步你會怎麼做?

 

ContactData 類別定義[copy code]
        public class ContactData        {            public string Name;            public string EmailAddress;            public string PhoneNumber;            public void OutputData(TextWriter writer)            {                writer.WriteLine("Name:\t{0}", this.Name);                writer.WriteLine("Email:\t{0}", this.EmailAddress);                writer.WriteLine("Phone:\t{0}", this.PhoneNumber);                writer.WriteLine();            }        }
   1:  public class ContactData
   2:  {
   3:      public string Name;
   4:      public string EmailAddress;
   5:      public string PhoneNumber;
   6:      public void OutputData(TextWriter writer)
   7:      {
   8:          writer.WriteLine("Name:\t{0}", this.Name);
   9:          writer.WriteLine("Email:\t{0}", this.EmailAddress);
  10:          writer.WriteLine("Phone:\t{0}", this.PhoneNumber);
  11:          writer.WriteLine();
  12:      }
  13:  }

 

開始來看看,有基本功夫的 programmer 跟一般 "熟 C# 熟 .NET" 的 programmer 差在那裡吧! 程式很簡單,先產生一百萬筆假資料,然後去找 A123456 這個人的資料,接著再找出手機號碼為 0928-1234 開頭的所有人資料。事後會分別計算花掉的時間跟程式佔用的記憶體大小。

 

1. 大概有 70% 的人,會選擇用 List<ContactData>,不為什麼,只因為他沒想到別的方法,或是直覺就覺的要這樣寫... 來看看這樣的 code:

用 List<ContactData> 寫的範例程式[copy code]
        private static void Sample1()        {            Stopwatch timer = new Stopwatch();            timer.Reset();            timer.Start();            // 產生假資料庫            List<ContactData> contacts = new List<ContactData>();            {                for (int index = 999999; index >= 0; index--)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    contacts.Add(cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = null;                data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }            timer.Reset();            timer.Start();            {                // 列出電話號碼為 0928-1234* 開頭的使用者                foreach (ContactData match in contacts.FindAll(delegate(ContactData x) { return x.PhoneNumber.StartsWith("0928-1234"); }))                {                    //match.OutputData(Console.Out);                }                Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            }            Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);        }
   1:  private static void Sample1()
   2:  {
   3:      Stopwatch timer = new Stopwatch();
   4:      timer.Reset();
   5:      timer.Start();
   6:      // 產生假資料庫
   7:      List<ContactData> contacts = new List<ContactData>();
   8:      {
   9:          for (int index = 999999; index >= 0; index--)
  10:          {
  11:              ContactData cd = new ContactData();
  12:              cd.Name = string.Format("A{0:D6}", index);
  13:              cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  14:              cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  15:              contacts.Add(cd);
  16:          }
  17:      }
  18:      Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  19:      timer.Reset();
  20:      timer.Start();
  21:      {
  22:          // 搜尋 A123456 這個人的資料
  23:          ContactData data = null;
  24:          data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });
  25:          Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  26:          //data.OutputData(Console.Out);
  27:      }
  28:      timer.Reset();
  29:      timer.Start();
  30:      {
  31:          // 列出電話號碼為 0928-1234* 開頭的使用者
  32:          foreach (ContactData match in contacts.FindAll(delegate(ContactData x) { return x.PhoneNumber.StartsWith("0928-1234"); }))
  33:          {
  34:              //match.OutputData(Console.Out);
  35:          }
  36:          Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  37:      }
  38:      Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);
  39:  }

 

憑良心說,寫的出這樣程式碼的人,已經算是高手了。因為這樣已經用到不少高級技巧,像是 delegate, anonums method, 還有知道 List<T>.Find( ) 怎麼用等等... 以下是他的執行結果:

image

 

 

2. 更進階一點的人 (另外 25%),也許會額外加上 Dictionary 當作索引,來改善 search A123456 這筆資料的效率...

加上 Dictionary 當作索引的 code[copy code]
            // 略            // 產生假資料庫            Dictionary<string, ContactData> name_index = new Dictionary<string, ContactData>();            List<ContactData> contacts = new List<ContactData>();            {                for (int index = 999999; index >= 0; index--)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    name_index.Add(cd.Name, cd);                    contacts.Add(cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = name_index["A123456"];                //data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }           // 略
   1:   // 略
   2:   // 產生假資料庫
   3:   Dictionary<string, ContactData> name_index = new Dictionary<string, ContactData>();
   4:   List<ContactData> contacts = new List<ContactData>();
   5:   {
   6:       for (int index = 999999; index >= 0; index--)
   7:       {
   8:           ContactData cd = new ContactData();
   9:           cd.Name = string.Format("A{0:D6}", index);
  10:           cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  11:           cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  12:           name_index.Add(cd.Name, cd);
  13:           contacts.Add(cd);
  14:       }
  15:   }
  16:   Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  17:   timer.Reset();
  18:   timer.Start();
  19:   {
  20:       // 搜尋 A123456 這個人的資料
  21:       ContactData data = name_index["A123456"];
  22:       //data = contacts.Find(delegate(ContactData x) { return x.Name == "A123456"; });
  23:       Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  24:       //data.OutputData(Console.Out);
  25:   }
  26:  // 略

 

會寫到這樣,也算強者了。不但對 C# 夠熟,也有用 Collection 物件來當作索引的觀念。程式碼只有兩行不同,一個是多宣告了 Dictionary 物件 (第3行),另一個是搜尋的地方 (第21行)。來看看執行結果:

image

 

果然有效,建資料從 5151ms 變成 5843ms, 記憶體從 288MB 變成 340MB,不過 search(A123456) 卻快的嚇人, 0ms... 破錶了!

 

但是這樣的 CODE 老實說只能算是及格而以,因為它沒有挑對 Collection 來用。怎麼說? 我的理由有這幾個:

  1. List<T> 的搜尋效能不好
  2. 沒能滿足用多種排序方式的要求 (需要時要當場執行 List<T>.Sort( ))

如果這是某個 Mail Client 內的 CODE,產品經理一定會問:

"如果資料從一百萬筆,變成一億筆,程式的表現會是什麼情況?"

 

有沒有基本功夫,這裡開始有差別了。唸過資料結構的都知道有個叫 "時間複雜度" (time complexity) 的東西,用 O(n) 表示。O(n) 代表花費的時間會跟資料比數成線性的成長。100倍的資料大概就要花上100倍的時間.. 如果是 O(n^2) 的演算法,則 100 倍的資料就會花上 10000 倍的時間。

MSDN 專業的地方就在這裡。Microsoft 真的很細心的在每一個 Collection 物件的說明文件上,都會標上 time complexity。有唸書有保佑,瞄到那行字我的問題就都解決掉了。先來看看 List<T> 的行為:

 

List<T>.Add(T item)

If Count is less than Capacity, this method is an O(1) operation. If the capacity needs to be increased to accommodate the new element, this method becomes an O(n) operation, where n is Count.

 

List<T>.FindAll(Predicate<T> match)

This method performs a linear search; therefore, this method is an O(n) operation, where n is Count.

 

再來看看 Dictionary<TKey, TValue> 的行為:

Dictionary<TKey, TValue>.Add(TKey key, TValue value)

If Count is less than the capacity, this method approaches an O(1) operation. If the capacity must be increased to accommodate the new element, this method becomes an O(n) operation, where n is Count.

 

Dictionary<TKey, TValue>.Item[TKey key] {get; set;}

Getting or setting the value of this property approaches an O(1) operation.

 

好,答案出來了。當資料變成一百倍時,List.Add 是 O(1), 所以每加一筆資料的時間不會越來越久 (safe). 但是搜尋時間是 O(n), 意思是現在找 A123456 要花 60ms, 未來有一億筆就要花 60x100=6000ms=6sec, 找 0928-1234* 則要花 240x100=24000ms=24sec... 以這樣的成長速度,記憶體還沒用完,你的程式就會慢到受不了了。有沒有其它的解決辦法?

 

換成 Dictionary 就酷多了,搜尋時間是 O(1), 代表不管你有幾筆,搜尋的時間都差不多。為什麼? MSDN 說的很清楚...

http://msdn.microsoft.com/en-us/library/xfhwa508.aspx

The Dictionary<(Of <(TKey, TValue>)>) generic class provides a mapping from a set of keys to a set of values. Each addition to the dictionary consists of a value and its associated key. Retrieving a value by using its key is very fast, close to O(1), because the Dictionary<(Of <(TKey, TValue>)>) class is implemented as a hash table.

 

什麼是 HashTable? 又是一個好例子,唸過資料結構的都知道吧? 我就不多說了,請看 wiki:

http://en.wikipedia.org/wiki/Hashtable

 

一樣是看 MSDN 文件,有沒有唸過資料結構,到這裡就差這麼多了。體會到學校教的東西真的有用了嗎? 這個例子還沒完,再看下去。

 

事實上,以上的實作方式都不合格。LIST 效能不好,Dictionary 拿來作索引有個致命的缺點:

它的 KEY 不能重複!!!

是的,對應到資料庫的話,它就好像是個 unique key 一樣。拿來當 NAME 的索引還沒問題,拿來當其它欄位的索引就糟糕了,別說效能問題,連用都不能用。

另外,針對排序的問題也是無解,這是 HashTable 的特性,要照順序排,就要另請高明。

 

事實上,以上的實作方式都不合格。List 搜尋的效能不算好,而 Dictionary 也只能處理 exact match 的狀況,同時也無法處理需要排序的問題。

 

唸過書的再想想,這時該怎麼辦?

標準解法是分別照這幾個欄位排序,然後用 Binary Search. 這才是正解。因為排序好的資料就像一般資料庫的 index, 可以讓你很容易的 order by, 同時又能讓你很快的找到你要的資料,甚至是列出某一段範圍的資料都沒問題。

不過寫成程式要怎麼作? MSDN 就在手上嘛,System.Collection.Generic 就把它當購物網站,逛一逛... 看有沒有其它合用的。

 

不錯,又找到兩個: SortedListSortedDictionary,還是一樣,那一個比較合適? MSDN 都寫的很清楚,足夠你判斷了,前題是資料結構教的幾個基本觀念 (像是前面講的 Hash Table, Time Complexity 等) 人家寫出來你要看的懂,看的懂就知道該挑那一個。

 

至於挑選的過程我就不多說了。我最後決定用 SortedList, 列一下這個 Collection 的特性:

SortedList.Add( )

This method is an O(n) operation for unsorted data, where n is Count. It is an O(log n) operation if the new element is added at the end of the list. If insertion causes a resize, the operation is O(n).

 

新增一筆需要的時間是 O(n), 唯一特例是加在最後面,而且沒引發 resize 的動作,就是 O(log n)。至於排序? 通通是 O(1),因為在 Add() 把資料加進來時就排好序了,所以 Add() 花的 O(log n) 就是在排序。要照順序印資料或找資料,完全不費吹灰之力,拿來印就是了。不過比較可惜的是,SortedList 並沒有提供 BinarySearch,因此要找 "0928-1234*" 這樣的資料要辛苦點,自己用 BinarySearch 的邏輯,簡單寫一下吧。如果前面的關卡都過了,這應該不難吧?

改用 SortedList 最大的缺點就是載入資料時會比較慢,不過其它在程式的處理上,還有效能都更貼近這個題目的需求。來看看程式碼,這次我用了兩個 SortedList,分別代表替 name 及 phone number 作排序:

 

改用 SortedList<> 的範例[copy code]
        private static void Sample3()        {            Stopwatch timer = new Stopwatch();            timer.Reset();            timer.Start();            // 產生假資料庫            SortedList<string, ContactData> name_index = new SortedList<string, ContactData>();            SortedList<string, ContactData> phone_index = new SortedList<string, ContactData>();            {                for (int index = 0; index < 1000000; index++)                {                    ContactData cd = new ContactData();                    cd.Name = string.Format("A{0:D6}", index);                    cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);                    cd.PhoneNumber = string.Format("0928-{0:D6}", index);                    name_index.Add(cd.Name, cd);                    phone_index.Add(cd.PhoneNumber, cd);                }            }            Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            timer.Reset();            timer.Start();            {                // 搜尋 A123456 這個人的資料                ContactData data = name_index["A123456"];                Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);                //data.OutputData(Console.Out);            }            timer.Reset();            timer.Start();            {                // 列出電話號碼為 0928-1234* 開頭的使用者                for (int pos = BinarySearch<string, ContactData>(phone_index, "0928-1234");                    pos < BinarySearch<string, ContactData>(phone_index, "0928-1235");                    pos++)                {                    //phone_index.Values[pos].OutputData(Console.Out);                }                Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);            }            Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);        }        private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key)        {            return BinarySearch<TKey, TValue>(index, key, 0, index.Count - 1);        }        private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key, int start, int end)        {            if (start == end) return end;            int pos = (start + end) / 2;            int compareResult = index.Comparer.Compare(key, index.Keys[pos]);            if (compareResult == 0)            {                return pos;            }            else if (compareResult > 0)            {                return BinarySearch<TKey, TValue>(index, key, pos + 1, end);            }            else            {                return BinarySearch<TKey, TValue>(index, key, start, pos - 1);            }        }
   1:  private static void Sample3()
   2:  {
   3:      Stopwatch timer = new Stopwatch();
   4:      timer.Reset();
   5:      timer.Start();
   6:      // 產生假資料庫
   7:      SortedList<string, ContactData> name_index = new SortedList<string, ContactData>();
   8:      SortedList<string, ContactData> phone_index = new SortedList<string, ContactData>();
   9:      {
  10:          for (int index = 0; index < 1000000; index++)
  11:          {
  12:              ContactData cd = new ContactData();
  13:              cd.Name = string.Format("A{0:D6}", index);
  14:              cd.EmailAddress = string.Format("{0:D6}@chicken-house.net", index);
  15:              cd.PhoneNumber = string.Format("0928-{0:D6}", index);
  16:              name_index.Add(cd.Name, cd);
  17:              phone_index.Add(cd.PhoneNumber, cd);
  18:          }
  19:      }
  20:      Console.WriteLine("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  21:      timer.Reset();
  22:      timer.Start();
  23:      {
  24:          // 搜尋 A123456 這個人的資料
  25:          ContactData data = name_index["A123456"];
  26:          Console.WriteLine("搜尋 A123456 花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  27:          //data.OutputData(Console.Out);
  28:      }
  29:      timer.Reset();
  30:      timer.Start();
  31:      {
  32:          // 列出電話號碼為 0928-1234* 開頭的使用者
  33:          for (int pos = BinarySearch<string, ContactData>(phone_index, "0928-1234");
  34:              pos < BinarySearch<string, ContactData>(phone_index, "0928-1235");
  35:              pos++)
  36:          {
  37:              //phone_index.Values[pos].OutputData(Console.Out);
  38:          }
  39:          Console.WriteLine("搜尋 0928-1234* 資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds);
  40:      }
  41:      Console.WriteLine("共使用記憶體: {0}MB", Environment.WorkingSet / 1000000);
  42:  }
  43:  private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key)
  44:  {
  45:      return BinarySearch<TKey, TValue>(index, key, 0, index.Count - 1);
  46:  }
  47:  private static int BinarySearch<TKey, TValue>(SortedList<TKey, TValue> index, TKey key, int start, int end)
  48:  {
  49:      if (start == end) return end;
  50:      int pos = (start + end) / 2;
  51:      int compareResult = index.Comparer.Compare(key, index.Keys[pos]);
  52:      if (compareResult == 0)
  53:      {
  54:          return pos;
  55:      }
  56:      else if (compareResult > 0)
  57:      {
  58:          return BinarySearch<TKey, TValue>(index, key, pos + 1, end);
  59:      }
  60:      else
  61:      {
  62:          return BinarySearch<TKey, TValue>(index, key, start, pos - 1);
  63:      }
  64:  }

 

執行結果:

image

 

至於前面產品經理問的問題,各位就試著自己到 MSDN 找看看答案吧! 比較過之後,你就會瞭解為什麼我會挑選 SortedList .. 我只挑 SEARCH 時間來看,List 的搜尋是 O(n), 而 SortedList 的搜尋是排序過的資料作 BinarySearch, 找找書就知道是 O(log n), 分別來比較一下:

當 N 等於 1000000 時:

List: 3131861 ticks
SortedList: 39294 ticks (快 80 倍)

 

推算一下,N 放大為 100 倍 (100000000) 時:

List: 3131861 x 100000000 / 1000000 = 313186100 ticks
SortedList: 39294 x log 100000000 / log 1000000 = 52392 ticks (快 5978 倍)

 

看到了嗎? 換個 Collection 物件,對於 Search 這個動作,一百萬筆資料時差了 80 倍,當資料成長一百倍 (100000000 筆) 時,搜尋速度差異爆增為近 6000 倍! 這就是資料結構或是演算法的差異,這樣的差異已經大到其它地方最佳化怎麼作都補不回來的地步,唯一一個關鍵就是要用對演算法!

   

終於打完這篇了。沒想到前一篇寫一堆老生常談的話,這次又變成一堆 sample code 了。不過我的目的就是讓各位瞭解,基礎一定要顧好啊,不然寫程式是一定會碰到瓶頸的。這次從很簡單的需求,帶到資料結構的觀念,再帶到 MSDN 裡面特別標記的資訊...。看完後應該不會再有人說學校教的東西沒用了吧?

 

有網友問過我有沒有推薦什麼書? 很抱歉,我也只看過課本而以 ... 哈哈,這些純粹是出來工作後,無意間還想到要去翻翻課本得來的經驗。其實這種例子很多,過去我常貼的 multi-thread 的文章也是很多這樣的例子,只不過課本從資料結構換成作業系統了,這個主題才寫到 1-2, 後面還有, 有什麼看法或心得就請留在回應給我吧! 如果能支持一下旁邊的讚助商的話也算是種鼓厲啦.. 敬請期待下一篇..

--

調查一下,有人看這篇之前就知道 SortedList 嗎? 留個話給我吧,我很好奇這種東西有多少人會去用... :D



9/21/2008 10:30:00 PM

[C# yield return] #2. 另類的應用 - Thread Sync 替代方案

Microsoft.NET | 543 | C# | Threading | 小技巧 | 我的作品 | 技術隨筆 Facebook Share

上篇,講了一些 yield return 編譯後產生的 Code, 說明了 C# compiler 如何用簡單的語法替你實作了 IEnumerator 介面,而完全不會增加程式的複雜度,這是我認為 C# 提供最讚的 Syntax Sugar ...。

不過無意間我想到了 yield return 還有另一種應用方式。靈感來自之前 Darkthread 舉辦的 [黑暗盃程式魔人賽]。因為參賽題目 [xAxB猜數字遊戲] 原本就是考驗演算法,邏輯就不大簡單了,加上要配合 GameHost 的呼叫方式,難度更提高不少。因此之前貼了兩篇文章 [ThreadSync #1. 概念篇 - 如何化被動為主動?, #2. 實作篇 - 互相等待的兩個執行緒],介紹了我改寫的 AsyncPlayer,讓程式可以分別以獨立的執行緒執行 GameHost 及 Player 的程式碼。藉著這方式讓兩者都可以 "獨立思考",邏輯不會中斷,讓程式能夠簡單一些。

不過執行緒同步機制是很花時間的,因為兩方都要等來等去...。多了 Sync 的動作,就要至少 10 ms 的時間來完成這動作。跑個十幾萬次下來,額外花費的時間太多了,因此我貼了那兩篇文章後,就一直在思考這樣的作法有沒有其它效能較佳的方式?

有的,最後我找到的答案就是 yield return,不過大家看了一定很納悶...

"yield return (Iteration) 跟執行緒同步機制有什麼關聯?"

不多說,先看看之前畫的兩張時序圖:

image

先看之前 ThreadSync #1 裡提到的圖,我這次加上紅線當 "輔助線",紅線代表執行 GameHost 的主程式,這個執行序必需反反覆覆的在 GameHost / Player 兩份類別的程式碼跑來跑去,主程式是 GameHost 發起的,當然被強迫切成好幾段的就只有 Player 了。

image

這是修改過後的版本,GameHost / Player 有各自的執行緒,紅色是 GameHost,藍色是 Player。當執行緒跑到中間時代表它在等待了,等另一方也跑到中間把執行結果放到共用變數,同時叫醒對方之後才交換過來。兩方都各自照著自己的邏輯跑,不過這種等待 & 喚醒的動作,相較於一般的 function call / return 而言,實在是太慢了...。我就是從這張圖得來的靈感,這個解決方式不就跟 yield return 很像嘛? 都是為了避免多次呼叫之間,被呼叫的另一方的邏輯被破切斷的問題... 因此我就開始思考 AsyncPlayer 是不是有機會用 yield return 寫出另一個版本...。

原本的結構很直覺,透過共用變數來傳遞資訊,用 AutoResetEvent 來通知另一個等待中的執行緒可以醒來拿資料去用。而 yield return 則要換個角度來想這件事。yield return 是實作 Iterator 的一種方式,目的是讓你的程式自己決定如何把 collection 裡的 element 照什麼方式丟出去,原本的問題就要想成:

"GameHost 要跟 Player 拿所有 Player 會問的問題,而 Player 會透過 yield return 一次一次的把問題丟出去。"

看起來好像可行,不過方向只有單向,就是 Player 丟問題給 GameHost,還缺了 GameHost 把問題答案交給 Player 這段。不過這部份好解決,一樣用共用變數就搞定。細節我就不講太多,直接來看程式碼:

用 yield return 改寫過的 AsyncPlayer[copy code]
        public abstract IEnumerable<HintRecord> Think();        private HintRecord last_record = null;        public override int[] StartGuess(int maxNum, int digits)        {            base.StartGuess(maxNum, digits);            this._enum = this.Think().GetEnumerator();            this._enum.MoveNext();            return this._enum.Current.Number;        }        public override int[] GuessNext(Hint lastHint)        {            this._enum.Current.Hint = lastHint;            if (this._enum.MoveNext() == true) return this._enum.Current.Number;            throw new InvalidOperationException("Player Stopped!");        }        public override void Stop()        {            base.Stop();            this._enum.Current.Hint = new Hint(this._digits, 0);            try { this._enum.MoveNext(); }            catch {                Console.WriteLine("!!!!");            }        }        protected virtual HintRecord GameHost_AskQuestion(int[] number)        {            this.last_record = new HintRecord(                (int[])number.Clone(),                new Hint());            return this.last_record;        }        protected HintRecord GameHostAnswer        {            get            {                return this.last_record;            }        }
   1:  public abstract IEnumerable<HintRecord> Think();
   2:  private HintRecord last_record = null;
   3:  public override int[] StartGuess(int maxNum, int digits)
   4:  {
   5:      base.StartGuess(maxNum, digits);
   6:      this._enum = this.Think().GetEnumerator();
   7:      this._enum.MoveNext();
   8:      return this._enum.Current.Number;
   9:  }
  10:  public override int[] GuessNext(Hint lastHint)
  11:  {
  12:      this._enum.Current.Hint = lastHint;
  13:      if (this._enum.MoveNext() == true) return this._enum.Current.Number;
  14:      throw new InvalidOperationException("Player Stopped!");
  15:  }
  16:  public override void Stop()
  17:  {
  18:      base.Stop();
  19:      this._enum.Current.Hint = new Hint(this._digits, 0);
  20:      try { this._enum.MoveNext(); }
  21:      catch {
  22:          Console.WriteLine("!!!!");
  23:      }
  24:  }
  25:  protected virtual HintRecord GameHost_AskQuestion(int[] number)
  26:  {
  27:      this.last_record = new HintRecord(
  28:          (int[])number.Clone(),
  29:          new Hint());
  30:      return this.last_record;
  31:  }
  32:  protected HintRecord GameHostAnswer
  33:  {
  34:      get
  35:      {
  36:          return this.last_record;
  37:      }
  38:  }

程式碼一如往常,又是只有一點點 (謎之音: 你到底有沒有寫過長一點的程式碼? -_-) ...。 原本的 Think 改成會傳回 IEnumerable<HintRecord> 的型別,因此內部就可以透過一連串的 yield return xxxx; 指令來把問題交給 GameHost。而 GameHost 拿到題目就會開始計算答案,然後再呼叫 Player.GuessNext( ) 把上次的答案傳回去。透過 Player 的實作,GuessNext 會呼叫 _enum.MoveNext( ), 控制權會再交到 Think( ) 上次呼叫 yield return 的地方,直到又執行到下一個 yield return 為止。這時 GameHost 又取得下一個問題,不斷重複這樣的動作直到結束。

同樣的,我們用 DummyPlayer 改寫,看看用 yield return 的版本寫起來是怎麼樣?

DummyYieldPlayer 的程式碼[copy code]
    public class DummyYieldPlayer : YieldPlayer    {        private Random _rnd = new Random();        private int[] randomGuess()        {            int[] _currAnswer = new int[this._digits];            List<int> lst = new List<int>();            for (int i = 0; i < _digits; i++)            {                int r = _rnd.Next(_maxNum);                while (lst.Contains(r))                    r = _rnd.Next(_maxNum);                lst.Add(r);                _currAnswer[i] = r;            }            return _currAnswer;        }        public override IEnumerable<HintRecord> Think()        {            while (true)            {                yield return this.GameHost_AskQuestion(this.randomGuess());            }        }    }
   1:  public class DummyYieldPlayer : YieldPlayer
   2:  {
   3:      private Random _rnd = new Random();
   4:      private int[] randomGuess()
   5:      {
   6:          int[] _currAnswer = new int[this._digits];
   7:          List<int> lst = new List<int>();
   8:          for (int i = 0; i < _digits; i++)
   9:          {
  10:              int r = _rnd.Next(_maxNum);
  11:              while (lst.Contains(r))
  12:                  r = _rnd.Next(_maxNum);
  13:              lst.Add(r);
  14:              _currAnswer[i] = r;
  15:          }
  16:          return _currAnswer;
  17:      }
  18:      public override IEnumerable<HintRecord> Think()
  19:      {
  20:          while (true)
  21:          {
  22:              yield return this.GameHost_AskQuestion(this.randomGuess());
  23:          }
  24:      }
  25:  }

跟上次的 DummyAsyncPlayer (用 ThreadSync 的版本) 一樣,超簡單,實在沒什麼需要說明的了。唯一要特別記得的是,如果你需要取得 GameHost 傳回的答案,應該在 22 ~ 23 行之間,使用 this.GameHostAnswer( ) 來取得答案。有人問我為什麼不把它包成 function call ? 在 function 內接到參數後呼叫 yield return, 而把答案 return 回來不是很好嗎?

很無奈,除非 C# 支援像 C/C++ 那樣的 MACRO 語法,不然這個東西是不可能單靠 yield return 就做出來。你使用 yield return 的條件就是 function return type 一定要是 IEnumerable<T>,這是配對的,代表你不能任易的把 yield return 移到其它 function call 內。除非你不靠 C# yield return 來自動產生對應的 IEnumerator,一切自己來就可以。不過這樣不就又回到原點了? 咳咳... 就乖乖的寫兩行吧。

這樣的寫法執行效率就好的多,我用 DummyYieldPlayer 來測試,跟 DarkThread 提供的版本不相上下,意思是差異小到可以不理它的地步了 :D 這樣的方式不會有太大的效能損失,因為最後要執行的程式碼,跟直接手寫是差不多的,只是中間難寫的那段 code 是 C# compiler 幫我們解決掉,而不是像上回 AsyncPlayer 是用兩個執行緒來解決的。

效果很滿意,當然最後參賽的版本就改這寫法了 :D。不過寫的太晚,來不及幫到其它參賽者 :P,想到這方法算是我佔了 C# compiler 一點便宜,有幸找到方法坳 C# compiler 幫我把最難的部份寫好了,我自己則樂的輕鬆,專心研究怎樣才能少猜幾次... 這裡把我另類應用 yield return 的方法貼給各位參考一下,也算作個筆記 :D,各位高手如果還有發現 yield return 解決過你什麼樣的怪問題,也歡迎到我這留個言 :D



9/6/2008 3:28:00 AM

BlogEngine Extension: Secure Post v1.0

Microsoft.NET | ASP.NET | BlogEngine.NET | 我的作品 | 技術隨筆 | BlogEngine Extension Facebook Share

因為家裡大人開出條件,除非新的 BLOG 系統 (就是我在用的 BlogEngine 啦) 有特定文章要輸入密碼才能看的功能,否則她就不想換系統了 (原來是用 CommunityServer 2007)。要弄密碼其實很簡單,不過過去試過 IIS 加上整合式驗證... 弄到最後該看的人看不到,也沒擋到該擋的人而作罷...。

 

仔細想了想大人的需求,要的就是簡單的控制機制。不需要先建立帳號,也不需要登入,就是特定幾篇文章要輸入暗號才能看到內容,就這樣而以。無耐 BlogEngine 還算很年輕,替它寫的 Extension 也還不多,官方網站提供了幾個 Extension 列表,找到最接近的是這個: Password Protected Post... 不過它是以登入 BE 為使用者認證的方式,再依照 ROLE 跟 CATEGORY 的配對為授權方式,來控制那些讀者能看到那些文章...。就是不想要替每個人建帳號啊,看來只好自己寫了... Orz。

 

以往都是想要作什麼很簡單,難是難在把它作出來..。現在都反過來了,工具越來越強,系統也越來越完整,難的反而是思考要怎麼作,程式碼沒幾行就搞定了。之前的文章介紹過 BlogEngine 的 Extension 機制,這次就實際來試看看。我要寫的東西很簡單,就一組密碼就好,要有夠簡單的方式讓大人能夠指定那幾篇文章是要保護的,而所有的人 (已登入的除外) 只能看到提示輸入密碼的訊息,密碼打對了才會顯示文章內容。至於密碼要不要加密? 會不會被竊聽? 不重要啦,只要保護不要遜到按右鍵簡示原始碼,密碼跟內容都看光光了就好。

 

順手寫了幾行 CODE,先驗證一下最基本的動作做不做的到 (POC: Prove Of Concept)。第一步是先把顯示內容的動作攔下來,換成制示的輸入密碼訊息... 這個簡單,沒幾行就搞定了:

 

image

直接從 CodePlex 抓下來的 Source Code, 解壓縮完就可以寫了。加上這段 CODE 並不難,整個 Extension 只有這樣而以:

 

修改 POST 內容,改成提示輸出密碼的畫面[copy code]
[Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]public class SecurePost{    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);    }    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        StringBuilder bodySB = new StringBuilder();        {           // 略。透過 bodySB 輸出 HTML        }        e.Body = bodySB.ToString();    }}
   1:  [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
   2:  public class SecurePost
   3:  {
   4:      static SecurePost()
   5:      {
   6:          Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
   7:      }
   8:   
   9:      private static void Post_Serving(object sender, ServingEventArgs e)
  10:      {
  11:          Post post = sender as Post;
  12:          StringBuilder bodySB = new StringBuilder();
  13:          {
  14:             // 略。透過 bodySB 輸出 HTML
  15:          }
  16:          e.Body = bodySB.ToString();
  17:      }
  18:  }

 

 

看起來 CODE 還不少,不過算一算真正在作事的都是在湊那堆 HTML ... 關鍵只有一開始去攔 Post.Serving 事件,接到自己的事件處理器 Post_Serving( ) 上,之後所有會輸出 Post 內容的地方,都會觸發這個事件。然後只要在事件處理器內去調整 Post 內容就可以了。

 

好,好的開始是成功的一半,已經完成 1/3 了 (什麼???) 第一部份的 CODE 產生的 HTML,會引導使用者輸入密碼,按下 [GO] 之後,就會連到 POST 的網址了。不過除了原本網址之外 (post.AbsoluteLink) 後面還要加上 "?pwd=xxxxxx" 帶上使用者輸入的密碼。前面講過我只要最基本的防護,其它進階的安全問題就不理它了。我只要掌握兩個原則:

  1. 密碼一定要在 SERVER 端確認 (不能讓不知道密碼的人 view source 就找到密碼)
  2. 沒輸入密碼前不能在 CLIENT 端出現 POST 內容 (不能單純的用 DHTML 把文章內容 "藏" 起來)

另外補一件事,我也不要讓全部的文章都用這種機制保護。只要有特別標示的 POST 要密碼就好。看到 BlogEngine 內建的 BreakPost 這個擴充程式,我就仿照它的作法,內文找到特定字串就啟用。我定的規則是整篇 POST 內容開頭一定要是 "[password]" 才會啟用密碼保護機制。

既然這樣,第二步也很簡單。如果密碼對,一切照原狀顯示內容。密碼不對的話就一樣攔下來...。程式碼.... 只是在第一步的程式碼多了... 兩行...

 

加上檢查密碼的 CODE[copy code]
    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        if (HttpContext.Current.Request["pwd"] == Password) return;        if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;        StringBuilder bodySB = new StringBuilder();        {           // 略。透過 bodySB 輸出 HTML        }        e.Body = bodySB.ToString();    }
   1:  private static void Post_Serving(object sender, ServingEventArgs e)
   2:  {
   3:      Post post = sender as Post;
   4:      if (HttpContext.Current.Request["pwd"] == Password) return;
   5:      if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
   6:      StringBuilder bodySB = new StringBuilder();
   7:      {
   8:         // 略。透過 bodySB 輸出 HTML
   9:      }
  10:      e.Body = bodySB.ToString();
  11:  }

 

啥米? 就是第一部份的 CODE 加上第四及第五行就搞定了? 程式不挑的話,現在已經寫完了... 哈哈! 上面的輸入密碼畫面,輸入正確密碼後就可以看到文章內容了。我特地連網址列一起複製下來,在網址列上會看到密碼明碼。照道理應該是要先 HASH 啦,不過 CLIENT SIDE 跟 SERVER SIDE 都要有同樣的 HASH 機制才行,想用 MD5 / SHA256 之類的來算,無耐 CLIENT 要弄這些也是很煩,就決定不理它了...。明碼就明碼吧,執行後的畫面像這樣:

 

image

 

剩下的部份就沒什麼了,想想加上去好了。就是透過 BlogEngine 的 Extension Manager,讓使用者可以簡單的調整參數。要讓使用者自定的參數只有三個:

  1. 文章內容被保護時,要顯示的訊息
  2. 密碼提示
  3. 真正的密碼

這些東西自己做的話,就還得想要開檔案或寫資料庫,有點小囉唆,不過已經有 Extension Manager 了,只要在原本的 static constructor 再加幾行就搞定:

 

加上 Extension 接受的設定參數,及初始值[copy code]
    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);        ExtensionSettings settings = new ExtensionSettings("SecurePost");        settings.AddParameter(            "SecurePostMessage",            "顯示訊息:");        settings.AddParameter(            "PasswordHint",            "密碼提示:");        settings.AddParameter(            "PasswordValue",            "指定密碼:");        settings.AddValues(new string[] {            "本篇文章已受密碼保護,請依照題示輸入密碼。",             "一二三四",            "1234"});        settings.IsScalar = true;        settings.Help = "用密碼保護文章的內容。";        ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("SecurePost");    }
   1:  static SecurePost()
   2:  {
   3:      Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
   4:      ExtensionSettings settings = new ExtensionSettings("SecurePost");
   5:      settings.AddParameter(
   6:          "SecurePostMessage",
   7:          "顯示訊息:");
   8:      settings.AddParameter(
   9:          "PasswordHint",
  10:          "密碼提示:");
  11:      settings.AddParameter(
  12:          "PasswordValue",
  13:          "指定密碼:");
  14:      settings.AddValues(new string[] {
  15:          "本篇文章已受密碼保護,請依照題示輸入密碼。", 
  16:          "一二三四",
  17:          "1234"});
  18:      settings.IsScalar = true;
  19:      settings.Help = "用密碼保護文章的內容。";
  20:      ExtensionManager.ImportSettings(settings);
  21:      _settings = ExtensionManager.GetSettings("SecurePost");
  22:  }

 

我已經很努力的多撐幾行了... 不過也只有這廿行,寫完了...。整個 .cs 檔案直接丟到 ~/App_Code/Extension 就算安裝完成。用管理者身份登入 BE 後,在 Extension 那頁可以看到:

 

image

不錯,SecurePost 已經出現在 Extension Manager 裡了。因為有加上 settings 的程式碼,所以右邊有 [編輯] 的字樣出現。點下去之後會到這個畫面:

 

image

嗯,看起來真專業,沒想到從頭到尾所有的 CODE 還不到一百行...。幾十行 CODE 寫出來的 Extension 就可以唬人了.. :D,試看看還真的會動耶 (廢話)。早知道寫起來那麼快,當初就不花那麼多時間去找人家寫好的了...。最後附上整段完整的程式碼,有需要的人就拿去用吧! 用法很簡單,全部複製下來 (可以按 [COPY CODE] 就好),存檔,把檔案放在 ~/App_Code/Extension/SecurePost.cs 下,然後用管理者身份進入 BlogEngine Extension Manager 改一改就好了!

 

大功告成! 這個 Extension 如果對你有用的話就拿去用吧,要散佈也歡迎,不過只有個小要求,請不要把程式碼存到別的地方供人下載,請直接提供我這篇文章的網址就好。覺的好用就留個話給我,要幫我推一下文或讚助就更好了 :D,謝謝收看!

 

 

--

完整的 SecurePost.cs 程式碼[copy code]
using System;using System.Web;using System.Web.UI;using BlogEngine.Core.Web.Controls;using BlogEngine.Core;using System.Text;[Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]public class SecurePost{    private static string SecurePostMessage { get { return _settings.GetSingleValue("SecurePostMessage"); } }    private static string Password { get { return _settings.GetSingleValue("PasswordValue"); } }    private static string PasswordHint { get { return _settings.GetSingleValue("PasswordHint"); } }    private static ExtensionSettings _settings = null;    static SecurePost()    {        Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);        ExtensionSettings settings = new ExtensionSettings("SecurePost");        settings.AddParameter(            "SecurePostMessage",            "顯示訊息:");        settings.AddParameter(            "PasswordHint",            "密碼提示:");        settings.AddParameter(            "PasswordValue",            "指定密碼:");        settings.AddValues(new string[] {            "本篇文章已受密碼保護,請依照題示輸入密碼。",             "一二三四",            "1234"});        //settings.ShowAdd = false;        //settings.ShowDelete = false;        //settings.ShowEdit = true;        settings.IsScalar = true;        settings.Help = "用密碼保護文章的內容。";        ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("SecurePost");    }    private static void Post_Serving(object sender, ServingEventArgs e)    {        Post post = sender as Post;        if (HttpContext.Current.User.Identity.IsAuthenticated == true) return;        if (HttpContext.Current.Request["pwd"] == Password) return;        if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;        StringBuilder bodySB = new StringBuilder();        {            bodySB.AppendFormat(                "<b>{0}</b><p/>",                HtmlEncode(SecurePostMessage));            if (e.Location == ServingLocation.Feed)            {            }            else            {                bodySB.Append("<div>");                bodySB.AppendFormat(                    @"請輸入密碼(提示: <b>{0}</b>): <input id=""postpwd"" type=""password""/><button onclick=""document.location.href='{1}'+'?pwd='+escape(this.parentNode.all.postpwd.value);"">GO</button>",                     PasswordHint,                    post.AbsoluteLink);                bodySB.Append("</div>");            }        }        e.Body = bodySB.ToString();    }    private static string HtmlEncode(string text)    {        return HttpContext.Current.Server.HtmlEncode(text);    }}
   1:  using System;
   2:  using System.Web;
   3:  using System.Web.UI;
   4:  using BlogEngine.Core.Web.Controls;
   5:  using BlogEngine.Core;
   6:  using System.Text;
   7:   
   8:   
   9:   
  10:   
  11:  [Extension("SecurePost", "1.0", "<a href=\"http://columns.chicken-house.net\">chicken</a>")]
  12:  public class SecurePost
  13:  {
  14:      private static string SecurePostMessage { get { return _settings.GetSingleValue("SecurePostMessage"); } }
  15:      private static string Password { get { return _settings.GetSingleValue("PasswordValue"); } }
  16:      private static string PasswordHint { get { return _settings.GetSingleValue("PasswordHint"); } }
  17:   
  18:      private static ExtensionSettings _settings = null;
  19:   
  20:      static SecurePost()
  21:      {
  22:          Post.Serving += new EventHandler<ServingEventArgs>(Post_Serving);
  23:   
  24:          ExtensionSettings settings = new ExtensionSettings("SecurePost");
  25:   
  26:          settings.AddParameter(
  27:              "SecurePostMessage",
  28:              "顯示訊息:");
  29:          settings.AddParameter(
  30:              "PasswordHint",
  31:              "密碼提示:");
  32:          settings.AddParameter(
  33:              "PasswordValue",
  34:              "指定密碼:");
  35:   
  36:          settings.AddValues(new string[] {
  37:              "本篇文章已受密碼保護,請依照題示輸入密碼。", 
  38:              "一二三四",
  39:              "1234"});
  40:   
  41:          //settings.ShowAdd = false;
  42:          //settings.ShowDelete = false;
  43:          //settings.ShowEdit = true;
  44:          settings.IsScalar = true;
  45:          settings.Help = "用密碼保護文章的內容。";
  46:   
  47:          ExtensionManager.ImportSettings(settings);
  48:   
  49:          _settings = ExtensionManager.GetSettings("SecurePost");
  50:   
  51:      }
  52:   
  53:      private static void Post_Serving(object sender, ServingEventArgs e)
  54:      {
  55:          Post post = sender as Post;
  56:   
  57:   
  58:          if (HttpContext.Current.User.Identity.IsAuthenticated == true) return;
  59:          if (HttpContext.Current.Request["pwd"] == Password) return;
  60:          if (!e.Body.StartsWith("[password]", StringComparison.CurrentCultureIgnoreCase)) return;
  61:   
  62:   
  63:          StringBuilder bodySB = new StringBuilder();
  64:          {
  65:              bodySB.AppendFormat(
  66:                  "<b>{0}</b><p/>",
  67:                  HtmlEncode(SecurePostMessage));
  68:   
  69:              if (e.Location == ServingLocation.Feed)
  70:              {
  71:              }
  72:              else
  73:              {
  74:                  bodySB.Append("<div>");
  75:                  bodySB.AppendFormat(
  76:                      @"請輸入密碼(提示: <b>{0}</b>): <input id=""postpwd"" type=""password""/><button onclick=""document.location.href='{1}'+'?pwd='+escape(this.parentNode.all.postpwd.value);"">GO</button>", 
  77:                      PasswordHint,
  78:                      post.AbsoluteLink);
  79:                  bodySB.Append("</div>");
  80:              }
  81:          }
  82:          e.Body = bodySB.ToString();
  83:      }
  84:   
  85:      private static string HtmlEncode(string text)
  86:      {
  87:          return HttpContext.Current.Server.HtmlEncode(text);
  88:      }
  89:  }


9/2/2008 9:58:00 PM

[RUN! PC] 2008 九月號

Microsoft.NET | RUNPC | Threading | 我的作品 | 技術隨筆 Facebook Share

image

感謝編輯賞光,第三篇順利刊出 :D

 

執行緒這種東西,實在不是什麼主流的文章,不過雜誌社願意刊到第三篇,真是感謝... 前兩篇分別介紹了同步機制跟旗標,這次用執行緒集區作總結,提供了綜合的應用,也對效能的影響作整理,讓讀者具體的瞭解使用前後的效能差異。

 

這次文章內提到了 ThreadPool 的應用,不過因為內容及篇幅的關係,沒有挖到 ThreadPool 本身怎麼設計。對這部份有興趣的讀者可以參考我寫的這三篇:

ThreadPool 實作 #1. 基本概念

ThreadPool 實作 #2. 程式碼 (C#)

ThreadPool 實作 #3. AutoResetEvent / ManualResetEvent

 

雖然好像沒有人因為看到雜誌才連到這裡來,不過還是要囉唆一下,看到文章有任何意見都可以在這裡留言給我。文章內提到的 SAMPLE CODE 可以在這裡下載! 這次的範例程式是 Console application,不提供直接在網頁上執行,下載回去試試吧!



7/6/2008 4:13:00 AM

[BlogEngine Extension] PostViewCount 1.0

Microsoft.NET | ASP.NET | BlogEngine.NET | 我的作品 | 技術隨筆 | BlogEngine Extension Facebook Share

這篇拖好久了,本來上禮拜要寫,結果正好碰到 BlogEngine 1.4 RELEASE,就一直拖到現在...。之前找到一個給 BlogEngine 用的 Counter Extension,以功能來說還不錯用,不過用久了就開始不滿足了。正好翻到這篇教學文章,算是官方文章了吧 (BlogEngine 作者之一寫的教學文)? 所以就動起自己寫的念頭。舊的其實沒什麼不好,不過缺了這幾項我想要的功能:

  1. 只有計 Total Count (謎: 不然你要 counter 記什麼?)
  2. 資料檔的結構及 I/O 的設計有點 Orz...
    1. 讀寫 XML 的 CODE 寫的很... Orz
    2. 沒有處理同時讀寫的問題 (後面寫的資料可能會蓋掉前面的,我的點閱率不知道少了幾百次 :D)
    3. 要有 CACHE 來加速處理速度

 

既然要重寫,當然要寫個合用的. 底下是我對於新的 COUNTER 期望:

  1. 要能記流水帳。
    流水帳就是不只要記總數,我還要知道每次點閱的時間,來源 IP ... 等等
    ( darkthread 指示: 當你流量大的時後就不會去在意這個了... Orz, 真是一針見血... )
  2. 要處理多執行緒下讀寫資料檔的問題,這部份 Code 必需為 ThreadSafe。
  3. 妥善利用 CACHE,降低 (2) 的複雜度。
  4. COUNTER COMPACT
    配合 (1) 的需求,流水帳記錄太多的話也會造成問題,COUNTER要能適當的刪除舊的 HIT RECORD。

 

決定好後就動工了! 既然問題都圍繞在 data storage 上,先來看看原來的檔案格式:

原有的 ~/App_Data/PostViews.xml 片段:[copy code]
<?xml version="1.0" encoding="utf-8" standalone="yes"?><posts>    <post id="b43ec49e-e9a2-4696-bcc7-2ba1667ecda9">781</post>    <post id="f1411c11-11ed-4f35-b383-0c6c8b2b963a">603</post>    <post id="e7b57492-652b-4247-bcd4-bc3ac2e56318">589</post>    <post id="7e2c2c88-240c-40ea-8477-2c96880adc8e">556</post>    <post id="0fda9c32-d294-4f09-85cd-41dab8e677cb">678</post>    ......</posts>
   1:  <?xml version="1.0" encoding="utf-8" standalone="yes"?>
   2:  <posts>
   3:      <post id="b43ec49e-e9a2-4696-bcc7-2ba1667ecda9">781</post>
   4:      <post id="f1411c11-11ed-4f35-b383-0c6c8b2b963a">603</post>
   5:      <post id="e7b57492-652b-4247-bcd4-bc3ac2e56318">589</post>
   6:      <post id="7e2c2c88-240c-40ea-8477-2c96880adc8e">556</post>
   7:      <post id="0fda9c32-d294-4f09-85cd-41dab8e677cb">678</post>
   8:      ......
   9:  </posts>

 

很普通的格式,配合我的需求,新的檔案結構我打算改成這樣:

新的 ~/App_Code/counter/{post-id}.xml 檔的片段內容:[copy code]
<?xml version="1.0" encoding="utf-8"?><counter base="8828">  <hit time="2008-06-29T12:42:51" referer="" remote-host="66.249.73.185" user-agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" />  <hit time="2008-06-29T13:04:15" referer="http://www.google.com.tw/search?complete=1&amp;hl=zh-TW&amp;cr=countryTW&amp;rlz=1B3GGGL_zh-TWTW237TW238&amp;q=%E9%A6%99%E6%B8%AFg9&amp;start=30&amp;sa=N" remote-host="124.10.1.162" user-agent="Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9) Gecko/2008052906 Firefox/3.0" />  <hit time="2008-06-29T13:04:20" referer="" remote-host="66.249.73.185" user-agent="Mediapartners-Google" />  ......</counter>
   1:  <?xml version="1.0" encoding="utf-8"?>
   2:  <counter base="8828">
   3:    <hit time="2008-06-29T12:42:51" referer="" remote-host="66.249.73.185" user-agent="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" />
   4:    <hit time="2008-06-29T13:04:15" referer="http://www.google.com.tw/search?complete=1&amp;hl=zh-TW&amp;cr=countryTW&amp;rlz=1B3GGGL_zh-TWTW237TW238&amp;q=%E9%A6%99%E6%B8%AFg9&amp;start=30&amp;sa=N" remote-host="124.10.1.162" user-agent="Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9) Gecko/2008052906 Firefox/3.0" />
   5:    <hit time="2008-06-29T13:04:20" referer="" remote-host="66.249.73.185" user-agent="Mediapartners-Google" />
   6:    ......
   7:  </counter>

 

其中, /counter/@base 是給你作弊用的,這數字你填多少就是多少,然後再加上底下有幾筆 <hit /> 記錄,總數就出來了。很簡單的格式,而流水帳就是記在 <hit /> 這個 XML ELEMENT 的 ATTRIBUTES 上。目前記的有時間,IP,REFERRER 而以。等到網站人數暴增 (幻想嘛?),COMPACT的動作就免不了了,預留的 /counter/@base 就派上用場了。COUNTER 在必要時就能刪掉多餘的 <hit /> 記錄,再把刪掉的筆數加到 /counter/@base 上,讓最後點擊總數不變,又能控制記錄檔的大小。

 

邏輯確定後就可以開始動工了,不過另外要解決技術問題,就是 ThreadSafe 的部份。這部份讓我煩惱了一下,因為在 File System 就提供了 FILE LOCK 的機制可以用,不過取得 LOCK 失敗是會引發 EXCEPTION,而不是像其它的 THREAD 控制機制會 WAIT。因此最後我決定 FILE LOCK 的機制做第二層保障 ,主要的 LOCK 機制還是自己來,用基本的 Monitor 來實作。要實做 LOCK 的前題,是要有明確的 LOCK "對像"。兩個 thread lock 同一個物件,後面 lock 的 thread 會被 block 暫停執行,直到前一個先 lock 同一個物件的 thread 釋放之後才會被喚醒,因此要先解決兩個同 ID 的 COUNTER 能拿到同一個物件,才能實作 LOCK 機制。這物件就好幾位合適的候選人,第一種方法是 COUNTER 本身,第二種方法則是替每個 counterID 產生一個無用的物件,就單純拿來 LOCK 用。

 

要用 (1) 成本太高,一來就必需實作 Flyweight 這個 design pattern,這個設計模式並不難,只要實作 factory pattern再搭配個 Dictionary<string, Counter> 物件就可以搞定,但是我最後沒有選擇這個作法。因為最糟的情況下有可能會讓整個系統可能會用到的 counter 通通被放到這個 dictionary ,沒有被釋放回收的機會,因為沒有明確的時間點可以把這物件從 dictionary 移掉,移不掉的話永遠就會留著一個 object reference 指向這 COUNTER 物件,reference 只要存在,它就永遠不會被 GC 收掉...。不過這還是有解,只要用 WeakReference 就可以解決了,不過我只是要作個簡單的 Counter,搬出這一堆東西會不會太過頭了?

 

因此我選擇了第二個方法。一樣用 flyweight pattern,只不過我放的是拿來 lock 的物件。我是直接 new object() 就拿來用了,物件很小我就不用耽心建立太多個又不能回收的問題...,而 counter 就讓它回歸最簡單的用法,需要就 new 一個出來,用完就丟著等著被回收。

 

每次都是講的話比 CODE 還多,來看看程式碼:

 

COUNTER 物件的 SYNC 機制[copy code]
        // 所有 COUNTER 用的 SYNC 物件 DICTIONARY        private static Dictionary<string, object> _counter_syncroot = new Dictionary<string, object>();        // 取得這個 COUNTER 用的 SYNC 物件        private object SyncRoot        {            get            {                return Counter._counter_syncroot[this._counterID];            }        }        private Counter(string counterID)        {            this._counterID = counterID;            //            //  建立 SYNC 物件 (如果沒有的話)            //            lock (Counter._counter_syncroot)            {                if (Counter._counter_syncroot.ContainsKey(this._counterID) == false)                {                    Counter._counter_syncroot.Add(this._counterID, new object());                }            }            //  略 ....        }        public void Hit()        {            lock (this.SyncRoot)            {                //                //  LOCK 後再開始更新檔案內容。 程式碼 略...                //            }        }
   1:  // 所有 COUNTER 用的 SYNC 物件 DICTIONARY
   2:  private static Dictionary<string, object> _counter_syncroot = new Dictionary<string, object>();
   3:  // 取得這個 COUNTER 用的 SYNC 物件
   4:  private object SyncRoot
   5:  {
   6:      get
   7:      {
   8:          return Counter._counter_syncroot[this._counterID];
   9:      }
  10:  }
  11:  private Counter(string counterID)
  12:  {
  13:      this._counterID = counterID;
  14:      //
  15:      //  建立 SYNC 物件 (如果沒有的話)
  16:      //
  17:      lock (Counter._counter_syncroot)
  18:      {
  19:          if (Counter._counter_syncroot.ContainsKey(this._counterID) == false)
  20:          {
  21:              Counter._counter_syncroot.Add(this._counterID, new object());
  22:          }
  23:      }
  24:      //  略 ....
  25:  }
  26:  public void Hit()
  27:  {
  28:      lock (this.SyncRoot)
  29:      {
  30:          //
  31:          //  LOCK 後再開始更新檔案內容。 程式碼 略...
  32:          //
  33:      }
  34:  }

 

這問題解決掉後,剩下的就是單純把我要的邏輯實作出來,這部份我相信讀者的程度都不用我多講了,有需要的看 CODE 就看的懂。請直接下載最後面的程式碼就好。

 

另外一個要提一下的是跟 BlogEngine 本身 Extension 相關的,這部份也花了點時間研究該怎麼寫。BlogEngine 的 Extension 寫法比較特別一點,一般這種外掛都是採 Provider 的方式實作 ( factory pattern 加上 abstract class ),先定義好這個 Provider 能作什麼事,然後每個寫 Extension 的人就自己繼承這類別來修改,再靠 Factory 動態的建立正確的 Extension 物件來使用。不過在這部份 BlogEngine 採用完全不同的作法來設計它的架構: Event Handler。

 

Provider 依賴的是事先定義好的 ProviderBase (abstract class) 類別。這個類別定義了多少東西給底下的人覆寫,就決定了寫外掛的人能處理多少事。好處是簡單,架構清楚。缺點是能讓你擴充的功能,在 DESIGN TIME 就決定了,要多一個能 "擴充" 的地方,就得改 ProviderBase 類別定義,這很有可能會讓現有的 Extension 不能跑...。換成 EVENT 的方式就沒有跟程式碼綁的那麼緊了。多了新的功能,多定義一些事件就夠了。BlogEngine 就是採這種方式來實作它的 Extension .. 

 

另外比較特別的是,BlogEngine 替每個 Extension 規劃好存放設定的地方。 1.3 版是所有的 Extension 共用一個設定檔, 1.4 則是有獨立的設定檔可以用。不過這些改變倒是沒有影響到它提供的 API。對於 API 來說,設定檔提供每個 Extension 一個像是 DataTable 那樣的 data storage, 讓你自訂欄位名,型別,然後能讓你一筆一筆的加進去,可以有多筆資料,而 BlogEngine Runtime 會負責幫你管理好這些設定。

 

這部份帶入門就好,用法還是去查官方文件比較快。我簡單貼一下這部份 CODE 跟畫面上提供的設定頁面給大家看看:

準備設定值的 SCHEMA 及載入目前的設定值[copy code]
    public PostViewCounter()	{        Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);        ExtensionSettings settings = new ExtensionSettings("PostViewCounter");        settings.AddParameter(            "MaxHitRecordCount",             "最多保留筆數:");        settings.AddParameter(            "HitRecordTTL",             "最長保留天數:");        settings.AddValues(new string[] { "500", "90" });        //settings.ShowAdd = false;        //settings.ShowDelete = false;        //settings.ShowEdit = true;        settings.IsScalar = true;        settings.Help = "設定 counter hit records 保留筆數及時間。只有在筆數限制內且沒有超過保留期限的記錄才會被留下來。";                ExtensionManager.ImportSettings(settings);        _settings = ExtensionManager.GetSettings("PostViewCounter");    }
   1:    public PostViewCounter()
   2:  {
   3:        Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);
   4:        ExtensionSettings settings = new ExtensionSettings("PostViewCounter");
   5:        settings.AddParameter(
   6:            "MaxHitRecordCount", 
   7:            "最多保留筆數:");
   8:        settings.AddParameter(
   9:            "HitRecordTTL", 
  10:            "最長保留天數:");
  11:        settings.AddValues(new string[] { "500", "90" });
  12:        //settings.ShowAdd = false;
  13:        //settings.ShowDelete = false;
  14:        //settings.ShowEdit = true;
  15:        settings.IsScalar = true;
  16:        settings.Help = "設定 counter hit records 保留筆數及時間。只有在筆數限制內且沒有超過保留期限的記錄才會被留下來。";
  17:        ExtensionManager.ImportSettings(settings);
  18:        _settings = ExtensionManager.GetSettings("PostViewCounter");
  19:    }

 

 

對應的設定頁面:

image

 

 

 

最後講了半天,真正想自己動手寫的人應該不多吧 :D,只是想下載回去裝來用的人就不用聽我前面廢話一堆了,只要下載這檔案,放到 ~/App_Code/Extension 下,就安裝完成了... 咳咳,連安裝手冊都省了。檔案 COPY 好後就會在 Extension Manager 裡看到我寫的外掛,就可以開始用了。有任何意件歡迎留話給我 :D

 

檔案下載: http://columns.chicken-house.net/file.axd?file=PostViewCounter.cs



6/25/2008 2:48:00 AM

Bot Checker 回來了!

Microsoft.NET | 543 | ASP.NET | BlogEngine.NET | 我的作品 Facebook Share

哈哈,終於加回來了 :D

 

為什麼原本在 CS 上很簡單就加上去的 Bot Checker, 在 BE 上弄到現在才好? 原因只有一個,就是 BE 在張貼回應時用了不少 AJAX 的機制,變的要插一段 CODE 進去要追半天 @_@...

很諷刺的是,AJAX 其實是 Community Server 用的比較兇,到處都要來一下... 反而 BlogEngine.NET 就中規中舉多了,很多地方就都乖乖的用 PostBack.. 唯讀回應的地方很突兀,感覺好像是特別要現一下回應的那個 TEXT EDITOR 還有 BBCODE 預覽的樣子... 那邊的 CODE 弄的實在是有點亂...

也是之前幾次都沒認真追啦... 追到一半覺的煩就去逛網站了,哈哈... 今天狠下心把它解決掉了。只不過卡著 AJAX,又不想跟它奮鬥,所以有些地方就偷懶混過去了。什麼意思? 意思就是攤開 HTML 原始碼,這個 Bot Checker 也是防不了 Bot 啦,不過我就賭我的站還有我的 Bot Checker 沒大到有人願意寫 Bot 來攻擊我.. 哈哈.. 來看看效果:

image

 

當然,張貼出去的回應也會帶著 Bot Checker 的問題。只不過礙於 AJAX,一堆東西被迫要移到 CLIENT SIDE 來處理,這邊就偷懶,題目產生完就先填到 [評論] 欄了,各位在填回應時,不爽附上 "芭樂雞萬歲" 的話,還是可以把它刪掉...

 

image

 

 

--
題外話,在追 BE 的程式碼的過程中,發現 BE 也有 CAPTCHA ? 不過還真的找不到怎麼把它打開... 有興趣用 BlogEngine.NET 又想用正統的 CAPTCHA 驗證的人可以試看看



6/22/2008 10:58:19 PM

又被盜文了... :@

Microsoft.NET | 543 | 不爽 | 我的作品 | 技術隨筆 Facebook Share

該說人紅嘛? 看起來也不是,就是正好我貼的文章又被盜用了。除了上次 [這篇文章] 被盜貼到 BLOGGER 上之外,這次又發現另一篇被盜貼了。這次被盜貼的是我之前發表一篇 [原來 System.Net.Mail 也會有 Bug ] 的文章,說明我碰到 Microsoft .NET Framework 的 BUG 及我挖掘問題真相的技術文章...。

 

 

image

這次無意間用 GOOGLE 找相關文章找到的,兩篇都是在對岸的網站,一個是照文全貼,唯獨 "忘" 了註明出處。至少他還很忠實原味,範例程式中出現吳小皮吳小妹,還有他們在 chicken-house.net 的 EMAIL 都沒有改掉... 只是把它去頭去尾,翻成簡體中文就貼了上去..

 

 

 

image

另一篇不是貼在他自己的 BLOG,而是貼在 "百度知道" 上面賺點數... 什麼是 "百度知道" ? 就是有點像台灣的奇魔知識+,在上面回答解決問題,賺分數用的。有人碰到了同一個 BUG 解不出來,對岸也有同胞很熱心的找到我的文章可以解決他的問題,就貼了上去... 一樣不小心 "忘" 了註明出處...。這位對岸同胞比較好一點,除了翻成簡體中文之外,其它就都照貼,沒有幫我去頭去尾...。

 

其實寫 BLOG 就是要分享用的,也不要求說要啥版稅或費用的。轉貼文章很歡迎,不過就是要求個 "尊重" 而以。同一篇文章也有幫到一位對岸同胞,他有在我 BLOG 留回應,其實這樣很好啊,幫到他我自己也很有成就感。不過這樣不註明出處的轉貼,我分享的動力完全被破壞掉了,只是簡單的尊重這麼難嘛?

 

這兩個地方我都留了 comment,請該篇文章的主人註明出處,也不曉得有沒有用。我把他們的網站都抓圖下來了,想看的請自己到圖中挖網址,我就不想貼在文章內了。不想因為我貼了這篇又增加他們網站的點擊率,我也不想要有我這邊 "引用" 他們文章的記錄,算是我無言的抗議吧。

 

沒事,發發牢騷而以。希望以後不要再有這種狀況發生了...。



6/3/2008 3:04:00 PM

FlickrProxy #4 - 修正同步上傳的問題

Microsoft.NET | ASP.NET | 我的作品 Facebook Share

寫到這,越寫越拖抬了... 這次沒有加上任何 "新功能",有的只是修正使用上的一些問題而以...

 

首先,還是要感謝愛用者 小熊子 的回報,照片初次被下載時會觸發上傳到 Flickr 的動作,上傳完成才重新引導 Request 到 Flickr 存取照片。如果在這一連串動作尚未完成前,就有第二個 Request 跑來的話,那這張照片就會被傳到 Flickr 兩次...。Orz,枉費我還投搞這些並行處理的文章,怎麼可以犯這種錯...

就跟很多 BUG 一樣,難在沒想到,難在沒發現,難在不知道原因...,不然修正 BUG 倒是很簡單的事,感謝回報這個 BUG 的恩公... 找到問題後剩下的 ISSUE 就迎刃而解了,要做的就是把關鍵的程式碼包裝在臨界區 (CRITICAL SECTION) 內,以防這段 CODE 同時間執行多次。這段 CODE 不能太大,鎖定範圍太大會影響效能 (好不容易換了四核CPU,鎖太大就糟踏這顆 CPU 了...),最後找出關鍵應該是 [判定是否需要上傳到 FLICKR] 及 [建立 FLICKR 副本檔] 這兩個動作,一定要包括在內,拆開的話就不能保證結果正確了。

修正過的程式,加上 LOCK 鎖定部份程式碼[copy code]
            string flickrURL = null;            lock (this.GetType())            {                if (File.Exists(this.CacheInfoFile) == false)                {                    //                    //  CACHE INFO 不存在,重新建立                    //                    this.BuildCacheInfoFile(context);                    context.Response.AddHeader("X-FlickrProxy", "Upload");                }            }
   1:  string flickrURL = null;
   2:  lock (this.GetType())
   3:  {
   4:      if (File.Exists(this.CacheInfoFile) == false)
   5:      {
   6:          //
   7:          //  CACHE INFO 不存在,重新建立
   8:          //
   9:          this.BuildCacheInfoFile(context);
  10:          context.Response.AddHeader("X-FlickrProxy", "Upload");
  11:      }
  12:  }

 

 

寫好後,這段 CODE 越看越不順眼,雖然我的網站流量沒那麼大啦 [H],不過怎麼可以這麼短視... 這段 CODE 的問題還是一樣,鎖定的範圍 "太大了" !! 會嚴重影響效能.. (如果流量真的很大的話)

怎麼說? 不過才兩行,那到底是要縮到多小? 不不,問題其實不在於 LOCK 的區段到底有多少 CODE,而是該 LOCK 的只有對同一張照片的 Http Request 才該被阻檔下來,同時間有多個 Http Request 來下載不同張照片,以現在的點閱率來說我應該要高興吧... 幹嘛還去 LOCK 它? 不過上面的 CODE 就是在做這件事,不分青紅皂白只要是有上傳到 FLICKR 的動作就一率 LOCK。更糟的是如果有一張照片正在上傳中,其它照片的 Http Request 也都會被迫停下來等它傳完...

 

該要有個改進的版本了。LOCK過度也是初次碰到 Multi-threading Programming 的人很容易犯的錯誤,接下來看看第二個版本:

修正過的版本,只會LOCK同一個檔案的REQUEST:[copy code]
            lock (this.LockHandle)            {                if (File.Exists(this.CacheInfoFile) == false)                {                    //                    //  CACHE INFO 不存在,重新建立                    //                    this.BuildCacheInfoFile(context);                    context.Response.AddHeader("X-FlickrProxy", "Upload");                }            }
   1:  lock (this.LockHandle)
   2:  {
   3:      if (File.Exists(this.CacheInfoFile) == false)
   4:      {
   5:          //
   6:          //  CACHE INFO 不存在,重新建立
   7:          //
   8:          this.BuildCacheInfoFile(context);
   9:          context.Response.AddHeader("X-FlickrProxy", "Upload");
  10:      }
  11:  }

 

看起來只有第一行 LOCK STATEMENT 裡指定的物件不一樣而以。沒錯,這裡跟第一段 CODE 的差別只在於 LOCK 的標的物不一樣。同一個物件只能被 LOCK 一次,當物件被 LOCK 還沒放開時,第二個人想要 LOCK 同一個物件,很抱歉... 得先等第一個人願意放掉它才可以。LOCK不同物件就各不相干了。沒錯,這就是我要的邏輯。所以這個問題的關鍵在於,我如何讓每張照片有專屬的 "物件" 來 LOCK ?

檔名的字串物件? 不適合... 可能有多個字串值相同,但是是不同物件... FileInfo? 也不行,因為找不到文件會保證同一個檔案拿到的 FileInfo 物件會是同一個... 沒辦法,只好自己弄一個。來看一下 LockHandle 的實作:

LockHandle Property 實作的程式碼[copy code]
        private object LockHandle        {            get            {                string hash = this.GetFileHash();                lock (_locks)                {                    if (_locks.ContainsKey(hash) == false)                    {                        _locks.Add(hash, new object());                    }                }                return _locks[hash];            }        }        private static Dictionary<string, object> _locks = new Dictionary<string, object>();
   1:  private object LockHandle
   2:  {
   3:      get
   4:      {
   5:          string hash = this.GetFileHash();
   6:          lock (_locks)
   7:          {
   8:              if (_locks.ContainsKey(hash) == false)
   9:              {
  10:                  _locks.Add(hash, new object());
  11:              }
  12:          }
  13:          return _locks[hash];
  14:      }
  15:  }
  16:  private static Dictionary<string, object> _locks = new Dictionary<string, object>();

 

其實以值來說,拿檔名就足夠拿來示別了,只不過有大小寫的問題要處理。拿檔案的內容做 MD5 實在有點殺雞用牛刀,不過因為處理照片本來就需要算檔案的 MD5 了,現成的就拿來用一下...。這裡我簡單的做了個 Dictionary,就放沒什麼用的 OBJECT,我只要這個 PROCESS 在有生之年,同一個檔案都會拿到同一個 OBJECT 就足夠了...

 

 

都寫到這還有什麼不滿意的? 還是有... 哈哈。因為我在測試時有過一個頁面,同一頁會放一堆圖檔...。試想一下當我瀏覽這頁面會發生什麼事?

"同時間 IE 發出了數個 HttpRequest 來跟我的程式要圖檔,如果正巧都是第一次,嗯,有限的頻寬要一次上傳多張圖檔到 Flickr,不就更慢了?"

就算我的頻寬沒問題,同一瞬間這麼多 UPLOAD 的動作,引起 Flickr 的 "關切" 就不好了... 我是不是應該要限制一下同時上傳的數量才對? 就像 FlashGet 可以限制同時下載的數量一樣...

哈,不就是之前寫過的文章,用 SEMAPHORE ? 沒錯... 怎麼老覺的這篇像在替我其它文章打廣告用的... 事實上不見得要動用到 SEMAPHORE,如果你要限制的是一次只能一個 UPLOAD 動作,直接用各種的 LOCK 機制就夠了。如果你要限制並行的 UPLOAD 動作是兩個以上,甚至更動態隨著 LOADING 變化等等,才需要動用到 SEMAPHORE ...

既然要 DEMO,就 DEMO 實際一點的 CODE 吧。假設我要限制並行 UPLOAD 的數量不超過 2 個,則需要把 CODE 改成這樣。首先要先準備好 SEMAPHORE 物件:

準備 SEMAPHORE,事先插好兩根旗子[copy code]
        public static Semaphore FlickrUploaderSemaphore = new Semaphore(2, 2);
   1:  public static Semaphore FlickrUploaderSemaphore = new Semaphore(2, 2);

 

真正執行上傳動作的部份要加上 SEMAPHORE 的管控:

用 SEMAPHORE 控制同步執行的數量[copy code]
            FlickrUploaderSemaphore.WaitOne();            photoID = flickr.UploadPicture(this.FileLocation);            FlickrUploaderSemaphore.Release();
   1:  FlickrUploaderSemaphore.WaitOne();
   2:  photoID = flickr.UploadPicture(this.FileLocation);
   3:  FlickrUploaderSemaphore.Release();

 

嗯,真是小題大作,不過這種機會不拿來練習練習,真正碰到怎麼寫的出來? 如果各位對於在 ASP.NET 上怎麼使用 LOCK 及 SEMAPHORE 不大熟的,可以參考一下我投稿的文章... 萬分感謝 [:D]



5/21/2008 1:15:00 AM

FlickrProxy #3 - 終於搞定大圖網址錯誤的問題

Microsoft.NET | ASP.NET | 我的作品 Facebook Share

 

由於在使用 Flickr API 時, 老是碰到上傳成功後, 結果拿到的照片 URL 不能看的問題... 被它整了好久, 不過總算解決了 @_@... 原來 API 拿到的資料是錯的, 嘖...

說來話長, 不過既然花時間解決了就要記錄一下... 問題大概是這樣. 上傳照片完成之後可以拿到 photoId, 代表某一張放在 Flickr 上的照片. 之後透過 PhotosGetInfo(photoId) 這個 API 可以取得這張照片的相關資訊, 當然也有 MediumUrl, LargeUrl... 等等 properties 可以用.

很直覺嘛, 要秀大圖我就直接拿 LargeUrl 就好, 偏偏有時是好的, 有時是壞的... API 傳回來的東西應該不會錯吧? 我心裡是這樣想的. 不過跟 Flickr 網站的 url 對照一下才發現, 竟然有時是不同的... 一路追下去, google 跟作者在 codeplex 網站上的 forums 都找了, 才發現...

PhotosGetInfo( ) 抓到的只是一堆 ID, 然後用 Flickr 公布的網址格式 "湊" 出各種 URL. 然而過去 Flickr 層經有一次改變部份網址的格式, 當你的圖檔不是很大時, Flickr 判定沒有另外存一張大圖的需要了, 就直接跳到原圖 (original size). 而原圖的網址格式又不一樣, 因此當圖檔太小時, API 抓到的 LargeUrl 就會是錯的...

My God,.... 為了這種鳥問題害我多白了好幾根頭髮... 找到原因後找 solution 就簡單了. 因應這個問題, 也多了一組 API: PhotosGetSizes( ), 直接連回 Flickr 明確的查詢可用的 size 有幾種, 連同它的網址及一堆相關資訊一起傳回來... 改用這個 API 傳回的資訊, 結果就正確了, 沒有圖掛掉的問題... Orz

 

不能怪人家 API 寫的不好, 只能怪自己功課沒作足... 不看文件直接拿 API 就用, 看名字猜用法才會這樣.. code 改一改就 ok 了, 少了這個不確定性, 原本畫蛇添足加上去的確認圖檔的動作也不用了.. 貼一下修改前跟修改後的 code:

 

使用 PhotoInfo 物件 (可能會出現 photo not available) [copy code]
            PhotoInfo pi = flickr.PhotosGetInfo(photoID);            string flickrURL = null;            string size = null;            try            {                flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);                size = "medium";                flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);                size = "large";                flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);                size = "original";            }            catch { }
   1:  PhotoInfo pi = flickr.PhotosGetInfo(photoID);
   2:  string flickrURL = null;
   3:  string size = null;
   4:  try
   5:  {
   6:      flickrURL = this.CheckFlickrUrlAvailability(pi.MediumUrl);
   7:      size = "medium";
   8:      flickrURL = this.CheckFlickrUrlAvailability(pi.LargeUrl);
   9:      size = "large";
  10:      flickrURL = this.CheckFlickrUrlAvailability(pi.OriginalUrl);
  11:      size = "original";
  12:  }
  13:  catch { }

 

 

改用 PhotosGetSizes( ) API[copy code]
            foreach (Size s in flickr.PhotosGetSizes(photoID).SizeCollection)            {                XmlElement elem = null;                elem = cacheInfoDoc.CreateElement("size");                elem.SetAttribute("label", s.Label);                elem.SetAttribute("source", s.Source);                elem.SetAttribute("url", s.Url);                elem.SetAttribute("width", s.Width.ToString());                elem.SetAttribute("height", s.Height.ToString());                cacheInfoDoc.DocumentElement.AppendChild(elem);            }
   1:  foreach (Size s in flickr.PhotosGetSizes(photoID).SizeCollection)
   2:  {
   3:      XmlElement elem = null;
   4:      elem = cacheInfoDoc.CreateElement("size");
   5:      elem.SetAttribute("label", s.Label);
   6:      elem.SetAttribute("source", s.Source);
   7:      elem.SetAttribute("url", s.Url);
   8:      elem.SetAttribute("width", s.Width.ToString());
   9:      elem.SetAttribute("height", s.Height.ToString());
  10:      cacheInfoDoc.DocumentElement.AppendChild(elem);
  11:  }

 

嗯, 終於搞定. FlickrProxy 正式邁入實用的階段... 收工!

 

 

 






精選文章

RUN! PC 文章及範例下載
2008/11. 生產線模式的多執行緒應用
2008/09. 用ThreadPool發揮CPU運算能力
2008/06. SEMAPHORE在ASP.NET的應用
2008/04. 以ASP.NET開發同步WEB應用程式

如何學好 "寫程式" 系列
#1. 該如何學好 "寫程式" ??
#2. 為什麼 programmer 該學資料結構 ??
#3. 進階應用 - 資料結構 + 問題分析
#4. 你的程式夠 "可靠" 嗎?

#5. 善用 TRACE / ASSERT

安德魯是誰?

Andrew Wu | Create Your Badge

我喜歡鑽研物件導向、軟體工程及作業系統等相關技術。我會在這裡發表我的研究心得,也當作我自己的學習筆記。


Recent comments

Comment RSS