3/23/2010 2:16:52 AM

[TxF] #2. 先作功課 - 熟悉 P/Invoke 及 Win32 檔案處理...

C# | Microsoft.NET | MSDN | TxF | P/Invoke

 

其實這篇是多寫的,因為前一篇提到的 Transactional NTFS 官方只提供 Win32 API 而已,不提供包裝好的 managed code 用的 library... 因此現階段想始用它,P/Invoke 是逃不掉的... 這篇就先來複習一下,想要在 C# 裡呼叫 unmanaged code 該怎麼用吧。這邊的例子為了配合後面幾篇,就同樣的以檔案處理為例。

這篇我不想去長篇大論的討論 P/Invoke 那堆規則及語法,也不想去討論那堆 Marshal 的觀念等等... 想學好 P/Invoke 就別看我這篇了,應該去 MSDN 看... 這篇我只想交待一下該如何配合 windows api 來作檔案處理而已。因為這些是往後要用到 Transactional NTFS 必要的技巧,TxF 新的 API 都是跟 win32 標準檔案處理的 API 一一對應的,弄懂了如何用 win32 api 操作檔案,你大概就學會八成的 TxF 了... 想用 TxF ...  熟悉點 P/Invoke 是應該的...

 

先從最單純的 MoveFile 開始吧,它沒有扯到啥 pointer 或是 handle ... 算是最單純的例子.. 先來看看這 API 原生的樣子:

BOOL WINAPI MoveFile( __in LPCTSTR lpExistingFileName, __in LPCTSTR lpNewFileName );

 

 

這 API 做啥事就不用多說了,把 lpExistingFileName 這檔案搬到 lpNewFileName 去... 搬成功還是失敗,就用 BOOL 值把結果傳回來。這時就要透過 P/Invoke 的用法,想辦法產生一個可以對應到 unmanaged code 的 C# function ... 我先把宣告方式貼出來,再來解試 code ...

P/Invoke: MoveFile[copy code]
   1:  [DllImport("kernel32.dll")]
   2:  static extern bool MoveFile(string lpExistingFileName, string lpNewFileName);

 

寫過 C / C++ 的人,大概對 extern 這個 keyword 不陌生吧? extern 這修飾字代表這個 function 是外來的,C# 正好拿來用在這裡。外來的 DLL 都是 function 型態,所以一定是 static, 沒有綁著任何一個物件... 而標在上面的 Attribute: DllImport, 則是標上這個 function 是來自那個 DLL。

在這個例子裡,這樣就足夠了,直接在程式裡試著寫一段 C# (managed code) 來試看看結果吧:

P/Invoke Sample #1. MoveFile[copy code]
   1:  public class PInvokeTest
   2:  {
   3:      [DllImport("kernel32.dll")]
   4:      static extern bool MoveFile(string lpExistingFileName, string lpNewFileName);
   5:      public static void Main(string[] args)
   6:      {
   7:          string srcFileName = @"C:\file1.txt";
   8:          string dstFileName = @"C:\file2.txt";
   9:          Console.Write("move file: from [{0}] to [{1}] ... ", srcFileName, dstFileName);
  10:          if (MoveFile(srcFileName, dstFileName) == true)
  11:          {
  12:              Console.WriteLine("OK!");
  13:          }
  14:          else
  15:          {
  16:              Console.WriteLine("FAIL!");
  17:          }
  18:      }
  19:  }

 

程式執行前,看一下 C:\ 的 DIR *.TXT 指令執行結果:

image

沒錯,有 c:\file1.txt 這檔案... 接著來執行範例程式:

image

執行成功。再重新看一下 C:\ 的 DIR *.TXT 指令執行結果:

image

看來程式很順利的呼叫了 win32 api 裡定義的 MoveFile( ... ) ... 這種範例有點不入流,要處理檔案總不可能只有這樣吧? 接著我們再來看看需要 Open File 加上讀寫檔案內容的應用。

Windows 是個以 HANDLE 為主的作業系統,一般在寫 C / C++ 程式,都是以指標(POINTER)的方式在處理資料,但是在 windows 裡,作業系統提供的資料用的指標,則特別以 "HANDLE" 來稱呼,它比一般的 POINTER 來說多了一些管理的動作。因此你開啟的檔案,或是建立的視窗,通通都以 HANDLE 來稱呼,而不是單純的用 POINTER (雖然它也是個 POINTER 啦)。接下來的例子就來看看 HANDLE 的應用。

[copy code]
   1:  public class PInvokeTest2
   2:  {
   3:      [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
   4:      public static extern IntPtr CreateFile(
   5:             string lpFileName,
   6:             uint dwDesiredAccess,
   7:             uint dwShareMode,
   8:             IntPtr SecurityAttributes,
   9:             uint dwCreationDisposition,
  10:             uint dwFlagsAndAttributes,
  11:             IntPtr hTemplateFile
  12:             );
  13:      [DllImport("kernel32.dll", SetLastError = true)]
  14:      static extern bool CloseHandle(IntPtr hObject);
  15:      public static void Main(string[] args)
  16:      {
  17:          IntPtr pFile = CreateFile(
  18:              @"c:\file1.txt",
  19:              0x80000000,
  20:              0x00000001,
  21:              IntPtr.Zero,
  22:              3,
  23:              0,
  24:              IntPtr.Zero);
  25:          Stream fs = new FileStream(pFile, FileAccess.Read);
  26:          TextReader tr = new StreamReader(fs);
  27:          Console.WriteLine(tr.ReadToEnd());
  28:          tr.Close();
  29:          fs.Close();
  30:          CloseHandle(pFile);
  31:      }
  32:  }

 

這個例子裡,很 "神奇" 的把 unmanaged code 拿到的指標 (IntPtr), 丟給 System.IO.* 底下的 FileStream 來用,竟然還能成功的開啟檔案並且把資料讀出來... 沒錯,MS寫的東西本來就都是同一家人,System.IO 那堆東西就是這樣包出來的。 這段範例程式除了看起來複雜一點之外,跟那堆要查文件才知道是啥意思的 flags 之外,其它都很簡單,不過就 Open File, 然後讀出內容,接著 Close ...

較特別的是,在 P/Invoke 的世界裡,都用 struct System.IntPtr 這個型別,來代表 unmanaged 世界裡常用到的 POINTER.. 藉著這個型別,我們就可以把兩個世界的橋樑給搭起來。不過當你執行這個範例時,編譯器會給你一個很礙眼的警告:

 

'System.IO.FileStream.FileStream(System.IntPtr, System.IO.FileAccess)' is obsolete: 'This constructor has been deprecated.  Please use new FileStream(SafeFileHandle handle, FileAccess access) instead.  http://go.microsoft.com/fwlink/?linkid=14202'  

 

沒錯,這是老用法了,一如用指標有數不盡的缺點,新的語言都想盡辦法扔掉它 (POINTER: 我是無辜的...),連包裝過的 IntPtr 也不例外。在 .NET Framework 2.0 裡就不再建議你用這個建構式了,請改用 SafeFileHandle .... 既然在 windows 的世界裡,一切系統的資源都是以 HANDLE 來處理,自然的在 managed code 裡也有對應的型別,它就是 System.Runtime.InteropServices.SafeHandle 。接著再來看看改用 SafeHandle 的版本:

[copy code]
   1:  public class PInvokeTest3
   2:  {
   3:      [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
   4:      static extern SafeFileHandle CreateFile(
   5:          string fileName,
   6:          uint fileAccess,
   7:          uint fileShare,
   8:          IntPtr securityAttributes,
   9:          uint creationDisposition,
  10:          uint flags,
  11:          IntPtr template);
  12:      public static void Main(string[] args)
  13:      {
  14:          SafeFileHandle pFile = CreateFile(
  15:              @"c:\file1.txt",
  16:              0x80000000,
  17:              0x00000001,
  18:              IntPtr.Zero,
  19:              3,
  20:              0,
  21:              IntPtr.Zero);
  22:          Stream fs = new FileStream(pFile, FileAccess.Read);
  23:          TextReader tr = new StreamReader(fs);
  24:          Console.WriteLine(tr.ReadToEnd());
  25:          tr.Close();
  26:          fs.Close();
  27:          pFile.Close();
  28:      }
  29:  }

 

執行的結果,當然也是順利的把文字檔內容印到 CONSOLE 裡了,我就不再多貼,直接看程式碼。

其實這裡有點偷吃步,用的是 SafeFileHandle, 而不是 SafeHandle。雖然兩者有繼承關係啦。不過後面我就都把它當成 SafeHandle 看待。這個版本的程式,除了把 IntPtr 換成 SafeFileHandle 之外,及最後的 CloseHandle( pFile ) 改成直接呼叫 SafeHandle.Close( ) 之外,沒有太大的不同了。有啦,把 IntPtr 跟 CloseHandle 兩者包裝在同一個 SafeHandler 裡是安全的多,至少 SafeHandle 實作了 IDispose 的介面,在適當的情況下,它至少會自動的被呼叫 Dispose( ) 回收資源...

 

看到這裡,以前老搞不清楚的 FileStream 為什麼有幾個怪怪的 constructor, 自從研究了 TxF 之後,意外的晃然大悟... 算是額外的收穫吧! 原來就是要配合 P/Invoke 使用...

其實說穿了,呼叫 win32 api 大概就這幾招了,幾種 unmanaged code 裡用到的型別,指標都可以對應之後,程式寫起來就簡單了。這篇我特意漏掉一部份,就是各種用 uint 來當作 flags 的型別,沒有轉到對應的 enum 列舉型別,我是覺的這是 option 啦,畢竟查查文件就有,多列上來我得多打好多字... 篇幅有限 XD

這幾個例子如果都看懂後,那麼 TxF 就沒有障礙了! 接下來就看下回分解了 :D    下回會看到第一個 "交易式" 的檔案處理範例! 沒灌 windows vista / 2008 / 7 / 2008 R2 的讀者門,快去準備吧!

 

參考資訊:

  1. 有用的網站: PINVOKE.NET
    http://www.pinvoke.net/index.aspx

    這網站幫你整理好了各種 win32 api 該如何宣告它的 C# signature, 以供 p/Invoke 使用。如果你不熟它的語法,這網站可以幫你不少忙。另外它也提供了一些方便的工具,像是 winform 的查詢工具,或是 visual studio 用的 add-ins
  2. MSDN 的 API 說明: MoveFile
    http://msdn.microsoft.com/en-us/library/aa365239(VS.85).aspx
  3. 什麼是 P/Invoke ? Wiki 的說明:
    http://en.wikipedia.org/wiki/Pinvoke
  4. Safe Handle & Critical Finalization
    (實在是看不懂的中譯... "安全控制代碼和關鍵結束" ???)
    http://msdn.microsoft.com/zh-tw/library/fh21e17c(v=VS.90).aspx


3/18/2010 12:06:00 AM

[TxF] #1. 初探 Transactional NTFS

C# | Microsoft.NET | MSDN | SQL | 作業系統 | TxF | Transactional NTFS

其實想用 TxF (Transactional NTFS, 交易式的 NTFS) 已經很久了,不過老是被一些雜事卡著,到過年期間才有空好好研究一下。這篇主要是介紹而已,就不講太多 Code, 先以瞭解 TxF 是什麼,及如何善用它等等方面為主。詳細的用法,就等後面幾篇吧!

Transactional NTFS, 中文是 "交易式NTFS",或是常見到的縮寫 "TxF”,早期的相關文章也有人寫 "TxFS”。這是在 Windows Vista / 2008 推出時,首次正式提供的功能。雖然它叫作 Transactional “NTFS”, 實際上它並不是一個新的檔案系統,而是一組新的 API (跟原有的檔案處理 API 幾乎是一對一的對應),  支援你用交易的方式操作檔案。一起推出的還有 Transactional Registry (TxR),一樣是有對應的 windows API,只不過它處理的對像是 windows registry,不是檔案...。

用這種方式處理檔案的讀寫動作,有種很神奇的感覺,過去都只在資料庫裡有機會這樣用,現在檔案的處理也可以了。配合像是 DTC 這類交易協調器的支援,甚至可以把檔案的處理及資料庫的處理,通通都包裝成一個交易來進行,一但任何一個環節失敗,都可以回復到最初的狀態,感覺好像是在用 DB,而不是寫檔案... 目前官方並沒有推出 managed code 的含式庫,現在要用只有幾種選擇:

  1. 直接用 C / C++ 呼叫 win32 api
  2. 用 P/Invoke,在 C# 裡呼叫 win32 api
  3. 找那些別人 (非官方) 包裝好的 .net class library ..

這些用起來都有點不踏實,畢竟用 P/Invoke 不是長久之計,總覺的遲早會被替換掉。不過即使如此,這項還是掩蓋不了這技術的價值。我貼一段自己寫的 sample code,讓還沒用過的人體會一下,寫檔案還支援交易處理的 "爽度" ...

 

[copy code]
   1:  // 建立 KTM transaction object
   2:  IntPtr transaction = CreateTransaction(
   3:      IntPtr.Zero,
   4:      IntPtr.Zero,
   5:      0, 0, 0, 0,
   6:      null);
   7:  string[] files = new string[] {
   8:      @"c:\file1.txt",
   9:      @"c:\file2.txt",
  10:      @"c:\file3.txt"};
  11:  try
  12:  {
  13:      foreach (string file in files)
  14:      {
  15:          // 使用支援交易的 delete file API
  16:          if (DeleteFileTransactedW(file, transaction) == false)
  17:          {
  18:              // 刪除失敗
  19:              throw new InvalidOperationException();
  20:          }
  21:      }
  22:      // 認可交易
  23:      CommitTransaction(transaction);
  24:  }
  25:  catch (Exception ex)
  26:  {
  27:      // 還原交易
  28:      RollbackTransaction(transaction);
  29:  }
  30:  CloseHandle(transaction);

 

範例裡用到的幾個 method, 像是 CreateTransaction( ), DeleteFileTransactedW( ), CommitTransaction( ), RollbackTransaction( ) ... 等等,都是透過 P/Invoke 的方式呼叫的 win32 api... 除了用的型別不如 pure .net class library 般直覺之外,這樣的 code 也已經很簡單了,短短卅行就可以搞定...

雖然這樣的 code 實在不大合我胃口,但是它畢竟是個堪用的方案... 對於 code 有潔癖的,可以考慮其它的用法。前面是最基本的 API call,如果你不滿意,MS自家的技術 DTC (Distributed Transaction Coordinator) 當然也支援 TxF。DTC 可以提供額外的好處,就是允許你做分散式的交易管理。意思是你配合 DTC,就可以把 Local File I/O 跟 database access 整合在同一個交易範圍內。

這邊的 sample code 我就不貼了,在 managed code 裡去呼叫到 COM 的那堆介面 (啥 QueryInterface 的) 實在跟 .NET programming 的 style 有點格格不入... 在 C# 的世界裡,應該用 TransactionScope 才對。在 MS 的世界裡,TxF + TxR + DB 都可以是 TransactionScope 內的一部份。這部份的 Sample Code 我一樣先不貼了,不然貼一堆 code 又沒篇幅說明,感覺很混...

其實,MS 該做的都做了,唯一缺的就是它竟然沒正式的併入 .NET Framework 內的一員... 如果 TxF 真的是你想用的東西,倒是有個 OpenSource Project 可以考慮一下: AlphaFS, 它的目標是能替換掉 namespace System.IO.*, 所以很多你常用的 class library, 它都有對等一樣用法的版本,當然它提供了更多的功能及改善... 其中 TxF 的支援就在內,你想用 TxF 來開發軟體的話,這是個不錯的選擇...

總之,這篇只是個開始,目的是想先 "預覽" 一下 TxF 的能耐,及未來它配 DTC / TransactionScope 後,能怎麼應用它的方式,還有其它可用的相關資源。接下來我會陸續整理一些相關的研究心得.. (別太期待,大概一兩週生一篇就很偷笑了 XD),下回見 !

 

 

參考資訊:

  1. AlphaFS: Brining Advanced Windows FileSystem Support to .NET
    http://alphafs.codeplex.com/
  2. MSDN magazine (July 2007): Enhance Your Apps With File System Transactions
    http://msdn.microsoft.com/en-us/magazine/cc163388.aspx
  3. B# .NET BLOG: Windows Vista - Introducing TxF In C#
    Part 1: Transacted File Delete
    Part 2: Using System.Transactions and the DTC
    Part 3: CreateFileTransacted Demo
  4. Code Project: Windows Vista TxF / TxR
    http://www.codeproject.com/KB/vista/KTM.aspx
  5. BLOG: Because we can
    http://blogs.msdn.com/because_we_can/archive/2005/05/18/419809.aspx
    Discussion and explanation relating to the Transactional NTFS feature coming in Longhorn, plus any other interesting anecdotes...
  6. Performance Consoderations for Transactional NTFS
    http://msdn.microsoft.com/en-us/library/ee240893(VS.85).aspx
  7. When to Use Transactional NTFS
    http://msdn.microsoft.com/en-us/library/aa365738(VS.85).aspx


12/19/2009 11:47:05 PM

[設計案例] 清除Cache物件 #2. Create Custom CacheDependency

543 | C# | CS | Microsoft.NET | MSDN | 小技巧 | 技術隨筆 | 物件導向

上一篇廢話了這麼多,其實重點只有一個,我這次打算利用 CacheDependency 的機制,只要一聲令下,我想移除的 cache item 就會因為 CacheDependency 的關係自動失效,而不用很辛苦的拿著 cache key 一個一個移除。

我的想法是用 tags 的概念,建立起一套靠某個 tag 就能對應到一組 cache item,然後將它移除。開始之前先來想像一下 code 寫好長什麼樣子:

透過 tags 來控制 cache items 的範例程式[copy code]
   1:      static void Main(string[] args)
   2:      {
   3:          string[] urls = new string[] {
   4:              "http://columns.chicken-house.net/",
   5:              // 共 50 組網址... 略
   6:          };
   7:          foreach (string url in urls)
   8:          {
   9:              DownloadData(new Uri(url));
  10:          }
  11:          Console.ReadLine();
  12:          TaggingCacheDependency.DependencyDispose("funp.com");
  13:          Console.ReadLine();
  14:      }
  15:      private static void Info(string key, object value, CacheItemRemovedReason reason)
  16:      {
  17:              Console.WriteLine("Remove: {0}", key);
  18:      }
  19:      private static byte[] DownloadData(Uri sourceURL)
  20:      {
  21:          byte[] buffer = (byte[])HttpRuntime.Cache[sourceURL.ToString()];
  22:          if (buffer == null)
  23:          {
  24:              // 直接到指定網址下載。略...
  25:              buffer = null;
  26:              HttpRuntime.Cache.Add(
  27:                  sourceURL.ToString(),
  28:                  buffer,
  29:                  new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
  30:                  Cache.NoAbsoluteExpiration,
  31:                  TimeSpan.FromSeconds(600),
  32:                  CacheItemPriority.NotRemovable,
  33:                  Info);
  34:          }
  35:          return buffer;
  36:      }
  37:  }

 

 

這段 sample code 做的事很簡單,程式準備了 50 個網址清單,用 for-loop 一個一個下載。下載的 method: DownloadData(Uri sourceURL) 會先檢查 cache 是否已經有資料,沒有才真正下載 (不過下載的細節不是本篇要講的,所以就直接略過了...)。

而主程式的最後一行,則是想要把指定網站 ( funp.com ) 下載的所有資料,都從 cache 移除。為了方便觀看程式結果,我特地加上了 callback method, 當 cache item 被移除時, 會在畫面顯示資訊:

image

由執行結果來看,果然被移出 cache 的都是來在 funp.com 的網址... 接著來看看程式碼中出現的 TaggingCacheDependecny 是怎麼實作的。相關的 code 如下:

TaggingCacheDependency 的實作[copy code]
   1:  public class TaggingCacheDependency : CacheDependency
   2:  {
   3:      private static Dictionary<string, List<TaggingCacheDependency>> _lists = new Dictionary<string, List<TaggingCacheDependency>>();
   4:      public TaggingCacheDependency(params string[] tags)
   5:      {
   6:          foreach (string tag in tags)
   7:          {
   8:              if (_lists.ContainsKey(tag) == false)
   9:              {
  10:                  _lists.Add(tag, new List<TaggingCacheDependency>());
  11:              }
  12:              _lists[tag].Add(this);
  13:          }
  14:          this.SetUtcLastModified(DateTime.MinValue);
  15:          this.FinishInit();
  16:      }
  17:      public static void DependencyDispose(string tag)
  18:      {
  19:          if (_lists.ContainsKey(tag) == true)
  20:          {
  21:              foreach (TaggingCacheDependency tcd in _lists[tag])
  22:              {
  23:                  tcd.NotifyDependencyChanged(null, EventArgs.Empty);
  24:              }
  25:              _lists[tag].Clear();
  26:              _lists.Remove(tag);
  27:          }
  28:      }
  29:  }

 

30行不到... 其實程式很簡單,TaggingCacheDependency 繼承自 CacheDependency, 額外宣告一個靜態的 Dictionary<string, List<TaggingCacheDependency>> 來處理各個標簽及 TaggingCacheDependency 的關係,剩下的就沒什麼了。呼叫 DependencyDispose( ) 就可以通知 .NET Cache 機制,將相關的 cache item 移除。

用法很簡單,當你要把任何物件放進 cache 時,只要用 TaggingCacheDependency 物件來標示它的 tag:

把物件加進 Cache, 配上 TaggingCacheDependency ...[copy code]
   1:  HttpRuntime.Cache.Add(
   2:      sourceURL.ToString(),
   3:      buffer,
   4:      new TaggingCacheDependency(sourceURL.Host, sourceURL.Scheme),
   5:      Cache.NoAbsoluteExpiration,
   6:      TimeSpan.FromSeconds(600),
   7:      CacheItemPriority.NotRemovable,
   8:      Info);

在這個例子裡 (line 4), 直接在 TaggingCacheDependency 物件的 constructor 上直接標上 tags, 在此例是直接把網址的 hostname, scheme 兩個部份當作 tag, 未來就可以依照這兩種資訊直接讓 cache 裡的相關物件失效。

而要下令讓 Cache 內有標上某個 tag 的 cache item 失效,只要這行:

 

將標為 "funp.com" 的 cache item 設為失效的 cache item[copy code]
   1:  TaggingCacheDependency.DependencyDispose("funp.com");

 

結果就會如同上面的程式範例一樣,還留在 cache 的該網址下載資料,在這一瞬間通通都會被清掉...

 

用這種方式,是不是比拿到 key 再去呼叫 Cache.Remove( key ) 的方式簡單多了呢? 同時也能夠更快速的處理複雜的移除機制。其實運用 tagging 的方式只是一例,需要的話你也可以設計合適的 CacheDependency 類別。

以下是本篇文章的兩個附加參考檔案:

Download File - URL清單



12/19/2009 4:29:29 AM

[設計案例] 清除Cache物件 #1. 問題與作法

ASP.NET | C# | CS | Microsoft.NET | MSDN | 小技巧 | 技術隨筆 | 物件導向

每次心裡有什麼好點子想寫出來時,第一關就卡在想不出個好標題... 想來想去的標題,怎麼看就是既不顯眼又不聳動... 果然是個老實的工程師性格 =_= ...  這次要講的,是 .NET HttpRuntime 裡提供的 Cache 物件的操作心得。這個東西我想不用我多作介紹,大家都用到爛掉了吧? 不過好用歸好用,有個老問題其實一直困擾著我很久了...

" 我該怎麼手動的把某個物件從 cache 裡移除? "

老實說,這問題蠻沒水準的... 老叫別人要翻 MSDN,我自己怎麼沒翻? 不不... 容我花點篇幅先說明一下問題。Cache物件,是個典型的 Dictionary 型態的應用 (雖然它沒有 implement interface: IDictionary… ), 透過 key 就可以拿到 cached item. 要從 cache 裡移除某個 item, 簡單的很,只要用 Remove 這個 method, 一行就搞定了:

從 key 移除指定的 cache item[copy code]
   1:  HttpRuntime.Cache.Remove(“cache-key”);

別小看這一行,實作起來障礙還不少。首先,你得額外去記著 cache key 的值。當你要移除的 cache item 有多個的時後,或是移除的 items 之間的關係有點複雜時,這些 code 就不怎麼漂亮了。下一個問題是:

" 我該如何得知所有存在 Cache 內的 keys 有那些? "

這個問題單純的多,那些把 intelligent sense 當購物網站的人 (平常不看文件,只會按下 . 然後挑個順眼 method 來用的人),可能這次就碰壁了... Cache 物件不像一般的 Dictionary 一樣,有提供 Keys 這樣的 property ... 它藏在 GetEnumerator 這 method 內,它會把所有的 keys 給巡一遍,你需要所有的 keys 的話,可以這樣用:

跑過 cache 裡每一個 key[copy code]
   1:  foreach (string key in HttpRuntime.Cache) { 
   2:      // … 
   3:  }

不過這樣的風險也是蠻高的,誰曉得你拿到 key 後的下一秒,這個 cache item 還在不在 cache 內?

 

 

 

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

本文正式開始! 哈哈,前面那一段只是廢話 + 碎碎唸,現在才是正題。前面想表達的只是,因為 cache 的不確定性 (資料隨時都會被 remove), 操作起來變的要格外小心, 即使它用起來像一般的 Dictionary 一樣。

我舉個案例,來說明我應用 cache 的情況。假如我想實作一個簡單的 web browser, 透過網路下載資源是很慢的動作,每種 browser 都會有某種程度的 cache 機制。我們就拿 Cache 物件替代 IE 的 "temporary internet files” 目錄吧。這時很簡單,只要用 URL 當作 KEY,下載的 content 就當物件塞進去就好...

不過事情沒那麼簡單。如果程式運作了一陣子,我想提供使用者手動清除 "部份" cache 的功能的話,那該怎麼辦? 我舉幾種情況:

  1. 從 cache 裡刪除所有從某個特定網站 (如: columns.chicken-house.net) 下載的資料
  2. 從 cache 裡刪除所有特定類型的資料 (如: content-type 為 image/jpeg 的圖檔)
  3. 從 cache 裡刪除所有透過特定 protocol (如: https) 下載的資料

這樣的要求應該不算過份吧? 用前面提到的兩種作法,你會想哭吧 XD .. 用這些基礎,你大概只能選這幾種作法 (各位網友有好作法也記得提供一下):

  1. 自己另外管理所有下載過的 URL, 用盡各種適合的資料結構,讓你可以順利的挑出這些 match 的 key, 然後移除它。

    缺點: 都作這麼多,你乾脆自己重寫個 cache 機制好了... 何況時間一久,你管理的 key, 那些對應的資料搞不好老早就通通從 cache 裡清掉了...
  2. 聰明一點,用 regular expression … 從 GetEnumerator( ) 一筆一筆過濾出要移除的 URL, 然後清掉它...

    缺點: 這作法只會檢查還留在 cache 內的 URL,不過這樣的 cache 隨便也有成千上萬個,每次都要 looping 掃一次實在不怎麼好看... 有違處女座有潔癖的個性...

 

這些方法 code 寫起來實在不怎麼漂亮,我就不寫 sample code 了,請各位自行想像一下寫起來的樣子。抱歉,如果你用的正好是上面的作法... 那請多包含... :D   這些都是 workable 的作法,但是看起來就是沒什麼設計感;程式可以動,不過就效能、簡潔、可讀性、美感來看,就是覺的不夠精緻 @@。跟朋友討論到這個問題時,我想到一個爛主意...

" 用蠢方法,這些 cache item 先分好類,每一類去關聯一個檔案,設 CacheDependency … 要清掉時去 touch 一下這個檔案,一整組的物件就會自動被清出 cache 了…。”

老實說,我覺的這是個既聰明又愚蠢的作法。聰明的是它很漂亮的解決我要如何移除某一群 item 的問題...,愚蠢的是這種單純程式內可以解決的事,竟然要繞到外面不必要的 file system I/O 動作... 而這通常是最慢的...

 

--

咳,寫太晚,實際的程式碼明天待續...



8/16/2009 2:43:27 AM

HVRemote (Hyper-V Remote Management Configuration Utility)

543 | MSDN | 小技巧

被這東西搞了半天,過了幾個月後發現有善心人事寫了個工具,今天看到了特地來記一篇... 免的以後又忘了 @@

話說 Microsoft 從幾年前開始被一堆 security 的問題苦惱後,決定所有產品都把安全視為第一優先... 這是件好事啦,不過為了 security 問題,真的會把 MIS 及 DEV 的相關工作難度加上好幾倍... 今天這個就是一例: 在沒有 AD 環境下,如何遠端的管理 Hyper-V server ?

之前把家裡的 SERVER 升級到 Windows Server 2008 + Hyper-V, PC 升級為 Vista 後,當然很高興的抓了 Hyper-V 的遠端管理工具回來裝。想說大概跟以往的 MMC 一樣,輸入 SERVER 的資訊,帳號密碼打一打,就可以用了...

事情當然沒這麼簡單,不然就沒這篇了... 直接使用的結果當然只是丟個沒權限之類的訊息。GOOGLE 找了一下解決方式... 找到這文章 (有五篇,別以為很辛苦的把它照作就結束了,還有 part 2 ~ part 5 @@):

http://blogs.technet.com/jhoward/archive/2008/03/28/part-1-hyper-v-remote-management-you-do-not-have-the-requested-permission-to-complete-this-task-contact-the-administrator-of-the-authorization-policy-for-the-computer-computername.aspx

 

細節就不講了,要調整的步驟還真它X的多... 先在 CLIENT / SERVER 都建好帳號,防火牆要允許 WMI,DCOM... 再設定 WMI 相關的權限給指定的帳號,還有後續一堆安全相關的設定要開... 最後搞了半天,真的成功了,不過... 最近趕流行,把 Vista 換成 Windows 7... 真糟糕,這堆步驟又要來一次 @@

這次又找了一下解決方式,還是一樣有這堆設定要改,不過跟幾個月前找到的同一個 BLOG,版主真是個好人,他把他整理出來的步驟寫成了個工具: HVRemote.wsf … 沒錯,就只是個 script 而已,不過它可不簡單。先看一下它的網站:

http://blogs.technet.com/jhoward/archive/2008/11/14/configure-hyper-v-remote-management-in-seconds.aspx

http://code.msdn.microsoft.com/HVRemote

http://technet.microsoft.com/en-us/library/ee256062(WS.10).aspx

作者把上面那一大串的步驟都寫成 script 了,你只要把這 script 抓下來,放到 client / server 都執行一次,就搞定了 :D  真專業,還有一份很完整的操作說明 PDF 檔... 一定要推一下這個工具 (Y)

 

附帶提一下,Hyper-V 遠端管理工具是透過 MMC 來執行的,但是我喜歡像 Remote Desktop 那種簡單的作法,只要開個連線工具,驗證過之後就可以遠端桌面這樣... Hyper-V 也有提供這樣的工具。只要裝好管理工具,你的電腦就會有這檔案:

C:\Program Files\Hyper-V\vmconnect.exe

image

 

搞什麼,連登入視窗都弄的跟 Remote Desktop Client 一模一樣,有時不小心還真會弄錯 =_=

開起來後就是大家熟悉的 Hyper-V 遠端管理的畫面了。這工具只是讓你省掉從 MMC 去 connect VM 這些步驟而以,像 RDP 一樣開了就能用:

image

 

當然透過這工具,上面那堆設定步驟也要照做才會通啦,只是順帶提一下這個 tips 而已。有了 HVRemote 這工具,要設定遠端管理 Hyper-V VM 就更輕鬆了,有需要的人就參考看看吧!



12/10/2008 11:30:18 PM

XmlWellFormedWriter.WriteRaw( ) 的 Bug 後續發展

Microsoft.NET | 543 | C# | MSDN | TROUBLE SHOOTING | XML | 技術隨筆

一時順手,就按下 Visual Studio 2008 上面的 [Report Bug] 回報上一篇發現的 Bug, 沒想到 M$ 真的有回應耶... :D

反正 M$ 在 connect 裡的回應本來就公開的,我就順手貼一下:

 

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=386899&wa=wsignin1.0

 

Hello,
Originally the WriteRaw method was designed for XmlWriters that are formatting text XML into a file. In those cases the WriteRaw method can be used to write out an XML fragment that is already formatted and checked for well-formedness. It can also be used for writing text nodes with special character that have been already escaped and no further processing of the text is needed.
However, when we introduced the XmlWriter over XmlDocument/XDocument (accessed via XPathNavigator editing methods), the use of the WriteRaw method on top of XmlDocument became controversial. We had two options:
1.    Threat it as a text
2.    Parse it into nodes


The second option is very difficult (if not possible) to do. The XML fragment can we written out in multiple WriteRaw calls, so we could not assume that a single WriteRaw will contain a fully enclosed fragment. It can also be interleaved with other XmlWriter calls and nested many times – overall a very hard thing to implement properly. So that is why we have decided to treat the WriteRaw content as text, which is what you are seeing.
If you have an XML fragment in a string and you want to append it to XmlDocument, you can do it like this:


                XmlDocument doc = new XmlDocument();
                XmlElement rootElement = doc.CreateElement("root");
                rootElement.InnerXml = "<a/><a/><a/><a/><a/>";
                doc.AppendChild(rootElement);


Or if you really want to use the XmlWriter from XPathNavigator.AppendChild(), you can create an XmlReader over your fragment and use the WriteNode method on the XmlWriter:


                XmlDocument doc = new XmlDocument();
                XmlWriter writer = doc.CreateNavigator().AppendChild();
                writer.WriteStartElement("root");
                using (XmlReader r = XmlReader.Create(new StringReader("<a/><a/><a/><a/><a/>"), new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment } ) ) {
                    writer.WriteNode(r, false);
                }
                writer.WriteEndElement();
                writer.Close();
                doc.Save(Console.Out);


I hope this helps.
Thank you,
-Helena Kotas, System.Xml Developer

 

看來 M$ 認為這是權宜之計,不算 BUG,要 USER 就直接避掉了,只是他建議的解法剛好就是我上一篇用的,用 XmlReader, 搭上 ConformanceLevel.Fragment 的設定解決掉...

只不過,這種情況,不是應該丟出 NotSupportException 比較好嘛? 幹嘛拿個不適當的實作填進來?

 

提外話,現在 WEB 2.0 的時代,M$ 工程師除了寫 CODE 之外,也要負責回客戶的問題了? 真辛苦...



12/7/2008 4:35:27 PM

原來 System.Xml.XmlWellFormedWriter 有 Bug ..

Microsoft.NET | 543 | C# | MSDN | TROUBLE SHOOTING | XML | 小技巧

果然沒啥人知道的 code, bug 也會比較慢被抓出來 ... 兩個小時前我才貼了找到 XmlNodeWriter 的替代品,用了一下就被我挖到一個 BUG ... @_@

先來看看我的 Sample Code:

XmlTextWriter v.s. XmlWellFormedWriter[copy code]
   1:  // test xml text writer, correct result
   2:  // output: <?xml version="1.0" encoding="big5"?><root><a/><a/><a/><a/><a/></root>
   3:  {
   4:      Console.WriteLine("Using XmlTextWriter:");
   5:      XmlWriter writer = XmlWriter.Create(Console.Out);
   6:      writer.WriteStartElement("root");
   7:      writer.WriteRaw("<a/><a/><a/><a/><a/>");
   8:      writer.WriteEndElement();
   9:      writer.Flush();
  10:      Console.WriteLine();
  11:      Console.WriteLine();
  12:  }
  13:  // test xml node writer, wrong result
  14:  // output: <?xml version="1.0" encoding="big5"?><root>&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;</root>
  15:  {
  16:      Console.WriteLine("Using XmlWellFormedWriter:");
  17:      XmlDocument xmldoc = new XmlDocument();
  18:      XmlWriter writer = xmldoc.CreateNavigator().AppendChild();
  19:      writer.WriteStartElement("root");
  20:      writer.WriteRaw("<a/><a/><a/><a/><a/>");
  21:      writer.WriteEndElement();
  22:      writer.Close();
  23:      xmldoc.Save(Console.Out);
  24:      Console.WriteLine();
  25:      Console.WriteLine();
  26:  }

 

而這是程式的輸出畫面:

image

 

兩段 code 除了拿到的 XmlWriter 來源不同之外,用它寫 XML DATA 的方式是一致的,不過寫出來的 XML 則完全不同。看來兩種 XmlWriter 對於 WriteRaw(...) 的實作不大相同。而照 MSDN 上的說明來說,XmlTextWriter的行為是對的,XmlWellFormedWriter 則太雞婆了,沒事多作一次編碼...

 

該說運氣好嘛? 哈哈... 繼上次撈到一個 SmtpMail 的 Bug 之後,這次又撈到一個... 要用的人注意一下,不過即使有這個 Bug, 也不會影響它的地位啦,這 Writer 解決了我很大的困擾,動搖國本也要用下去... (咳... 不過是避開一個 API ...)

 

最後我改了用法,一方面 API 有 BUG 是一回事,另一方面直接用這 API 也很危險,因為 MSDN 說它不會去做內容的驗證,也就是說透過 WriteRaw( ) 寫進不合法的資料,會讓你整份輸出都毀了... 第二個原因比較重要,因此我換了一個替代作法, 類似 Pipe 一樣,把 XmlReader 讀到的東西都寫到 XmlWriter:

XmlCopyPipe 實作[copy code]
   1:  /// <summary>
   2:   /// 從 XmlReader 複製到 XmlWriter
   3:   /// </summary>
   4:   /// <param name="reader"></param>
   5:   /// <param name="writer"></param>
   6:   private static void XmlCopyPipe(XmlReader reader, XmlWriter writer)
   7:   {
   8:       if (reader == null)
   9:       {
  10:           throw new ArgumentNullException("reader");
  11:       }
  12:       if (writer == null)
  13:       {
  14:           throw new ArgumentNullException("writer");
  15:       }
  16:       while (reader.Read() == true)
  17:       {
  18:           switch (reader.NodeType)
  19:           {
  20:               case XmlNodeType.Element:
  21:                   writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
  22:                   writer.WriteAttributes(reader, true);
  23:                   if (reader.IsEmptyElement)
  24:                   {
  25:                       writer.WriteEndElement();
  26:                   }
  27:                   break;
  28:               case XmlNodeType.Text:
  29:                   writer.WriteString(reader.Value);
  30:                   break;
  31:               case XmlNodeType.Whitespace:
  32:               case XmlNodeType.SignificantWhitespace:
  33:                   writer.WriteWhitespace(reader.Value);
  34:                   break;
  35:               case XmlNodeType.CDATA:
  36:                   writer.WriteCData(reader.Value);
  37:                   break;
  38:               case XmlNodeType.EntityReference:
  39:                   writer.WriteEntityRef(reader.Name);
  40:                   break;
  41:               case XmlNodeType.XmlDeclaration:
  42:               case XmlNodeType.ProcessingInstruction:
  43:                   writer.WriteProcessingInstruction(reader.Name, reader.Value);
  44:                   break;
  45:               case XmlNodeType.DocumentType:
  46:                   writer.WriteDocType(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value);
  47:                   break;
  48:               case XmlNodeType.Comment:
  49:                   writer.WriteComment(reader.Value);
  50:                   break;
  51:               case XmlNodeType.EndElement:
  52:                   writer.WriteFullEndElement();
  53:                   break;
  54:           }
  55:       }
  56:   }

 

很好用的作法,就像過去需要 COPY XML 資料,最常見的就是把來源跟目的都用 XmlDocument 載入,直接用 ImportNode( ) 把 XML 片段資料搬到另一個 XmlDocument 再儲存。跟上一篇的原因一樣,看起來很蠢... 就想到這個作法,透過 XmlReader, 拿到的是已經 parsing 過的資料,直接寫到 XmlWriter。而我用的 Writer 正好又可避開重複作 parsing 動作的優點,正好這樣效能跟可用性都兼顧了... 經過 parsing, 至少寫出來的東西會安心一點...

 

把最後我的程式搭配這個 XmlPipeCopy 改一改:

用 XmlCopyPipe 取代 WriteRaw( )[copy code]
   1:  XmlDocument xmldoc = new XmlDocument();
   2:  XmlWriter writer = xmldoc.CreateNavigator().AppendChild();
   3:  writer.WriteStartElement("root");
   4:  XmlReaderSettings settings = new XmlReaderSettings();
   5:  settings.ConformanceLevel = ConformanceLevel.Fragment;
   6:  XmlReader reader = XmlReader.Create(
   7:      new StringReader("<a/><a/><a/><a/><a/>"),
   8:      settings);
   9:  XmlCopyPipe(reader, writer);
  10:  writer.WriteEndElement();
  11:  writer.Close();
  12:  xmldoc.Save(Console.Out);

 

試了一下,果然如預期的執行了 :D,結果也沒錯,還好 XmlWellFormedWriter 的 Bug 只存在於 WriteRaw... 閃開就沒事了:

image

 

 

其中有個陷阱,就是如何用 XmlReader 讀取 XmlFragment (可以有多個 ROOT 的 XML DATA)。其實這個解法跟程式碼,大部份都是這篇看來的,只不過在裡面加了個 LOOP 跟改了名字,各位覺的好用的話記得去謝原作者 Mark Fussell, 別謝錯人了 :D



11/18/2008 1:23:00 AM

Policy Injection Application Block 小發現...

Microsoft.NET | 543 | AOP | Application Block | C# | MSDN | 小技巧 | 技術隨筆 | 物件導向

因為工作的關係,最近正在研究 Enterprise Library 裡整合的 Patterns & Practices 介紹的各式 Application Block... 撇開其它的發現,有個東西一定要提一下,就是 Policy Injection ...

介紹文章我就不多說了,一樣網路一大堆,有興趣的可以看 MSDN 官方的說明。比較特別的是它的用法。當年剛開始研究 .NET 內建的 Role Based Security Control,才在讚嘆它的 code 寫起來真漂亮,只要加個 attribute, 就可以在 runtime 自動檢查呼叫時的身份是否滿足 attribute 的宣告,如下:

CAS範例程式: [copy code]
[PrincipalPermissionAttribute(SecurityAction.Demand, Role="Supervisor")]public void Foo() {    // ... }
   1:  [PrincipalPermissionAttribute(SecurityAction.Demand, Role="Supervisor")]
   2:  public void Foo() {
   3:      // ... 
   4:  }

 

不管你的 code 在那裡,只要呼叫這個 Foo method, 當時的身份 ( principal ) 如果不屬於 "Supervisor" 這個角色的話,就會引發 Security Exception... 當初看到這真是太棒了,我可以用宣告的方式來作安全控制,不需要在主程式裡加一堆囉哩叭唆的 code 來查權限...

不過當我開始研究如何 "自定" 這個行為,除了加上自己的安全機制之外,想更進一步的加上 Log 或是其它的檢查... 我才發現跟本辦不到。因為... 這行為是直接在 CLR 裡支援的啊,我可以加上一堆自定的 Attribute 掛上去,但是呼叫時完全不會觸發我的 code ...

之後研究過 AOP,發現 AOP 正是解決我這類問題的 Solution, 無奈那些 solution 都不大實際,就沒深入研究了。之後找到篇 MSDN 的文章,裡面提到 .NET Remoting 時,遠方會產生 Proxy, 同時 Client / Server 之間的溝通會介著中間傳輸層傳遞 IMessage 介面封裝的 message, 到另一端才會由 Proxy 解讀,然後用 Reflection 還原呼叫的動作... 利用 Proxy 在還原呼叫動作時,你就有機會插入你要的邏輯 (IMessageSink),做到跟上面例子類似的功能。

 

還是很不實際啊啊啊啊,我沒事也不會去用 .NET Remoting 啊,用不到的話這招對我也沒啥用 (大錯特錯!! 當年的我真是太過自信了 :~~~~) ... 這事就一直擱著了,直到...

最近在研究 Policy Injection Application Block 時,讓我看到了似曾相識的 code:

 

Policy Injection Sample Code #1[copy code]
[AuthorizationCallHandler("operation-name")]public void Deposit(decimal depositAmount){  balance += depositAmount;}
   1:  [AuthorizationCallHandler("operation-name")]
   2:  public void Deposit(decimal depositAmount)
   3:  {
   4:    balance += depositAmount;
   5:  }

 

 

這段 CODE 跟前面 CAS 的範例作用差不多,一樣是在 method 被呼叫前作一次權限的檢查。不同的是 AuthorizationCallHandlerAttribute 是自定的 (由 Security Application Block 提供的),它的作用比 ROLE 更進一階,是直接檢查授權的。之間的差別就如同 windows 大家都知道把 USER 加入 Administrators 角色的話,"預設" 就可以做大部份的事,但是你要在某個有 ACL 的物件 (如 NTFS 的檔案) 拒絕 Administrators 的存取也是可行的。前面 CAS 的例子就只是判定你是不是某角色的人,而這例子則是判定某個授權的定義允不允許你執行。

扯遠了,重點不在安全,重點是自定的 Code / Attribute 也可以這樣用啊! 由於我多年心裡的疑惑,挖出這段作法比研究 Policy Injection 更積極一點 (老闆對不起...) 哈哈,沒想到答案就在前文...

 

 

它ㄨ的!! 原來只是在 Local 使用 .NET Remoting ...

 

 

說穿了不值錢,你用的物件標上 Attribute 後,要透過它的建立方式 ( Create or Wrap ) 取得加料過的物件,再呼叫它就會有你預期的效果了。這加料過的物件,就是 System.Runtime.Remoting.Proxies.RealProxy 下的某類別啊啊啊啊... 意思是我拿這加料過的物件,就會透過 .NET Remoting 的方式去呼叫到我真正的物件,而 Policy Injection Application Block 正好就替我把我要作的動作給補上去...。

雖然心裡有被擺了一道的感覺,不過它的 code 包裝的真漂亮啊... 除了 Create 的方式由原本的 new .ctor( ) 改成它的 Create( ... ) 之外,其它就通通一樣了。更猛的是它還提供了幾個真的很實用的 CallHandler (就是呼叫時會加料的動作啦):

  • Authorization Handler
  • Caching Handler
  • Exception Handling Handler
  • Logging Handler
  • Performance Counter Handler
  • Validation Handler
  • Custom Pipeline Handlers

大部份的 Handlers 都望文生義,像是 Logging 就是呼叫時替你加一段 LOG,而 Performance Counter 則是呼叫時就替你戳一下 windows 內建的 performance counter, 讓你可以透過 performance monitor 看相關統計 (如你的 method 被呼叫過幾次... ),更神奇的是 Caching, 如果你的 method 跑的很慢,加上去之後甚至是 cache 裡已經有了上次的結果,這次呼叫就直接 return 了... (你還記得你寫過多少次資料不在 cache 內就 insert 進去的 code 嗎?) @_@

 

如果你看這篇期望看到啥 Enterprise Library / Policy Injection Application Block 的深入介紹的話,很抱歉... 我還沒那本事,哈哈... 再過陣子研究出心得,可能會寫幾篇吧...。 這類文章如果你不介意看英文的,官方的說明還有 QuickStart 的範例就夠你看了,可以參考看看,我就不獻醜了...。 這篇純粹是為了這 AB 解除了我多年來的遺憾,特地留下篇記念用的... :D



11/1/2008 9:10:19 PM

MSDN Magazine (NOV 2008) 好文推薦: Engineers Who Write

543 | MSDN | 技術隨筆

MSDN Magazine (Nov 2008) 有一篇文章: "Engineers Who Write",覺的寫的還不錯,就把 LINK 貼上來。這篇文章的 abstract 是這段:

http://msdn.microsoft.com/en-us/magazine/dd153757.aspx

 

Writing is good exercise for developers and software engineers. Richard Ward explains how writing can help you focus your thoughts and communicate more precisely.

 

內文就不多說了,大意是說為什麼多寫寫文章對工程師很有幫助,看了覺的很有道理,其實裡面的原因也是我開始寫 BLOG 的原因。我最近比較常寫篇幅長一點的內容,一方面我想的東西都比較古怪,太短的篇幅講不完,另一方面內容多一點比較能訓練組織能力...。

技術出身的人常有個盲點,你想到很棒的點子,但是別人完全搞不懂你在想什麼...。另一種例子是你想做的東西,別人也聽不懂,或是聽懂了但是抓不到精髓,最後只能自己默默的把它完成。多寫寫文章有助於改善這種狀況,算是個技術人很值得投資的第二專長 :D



10/14/2008 2:52:00 AM

NGenerics - DataStructure / Algorithm Library

Microsoft.NET | 543 | C# | MSDN | 小技巧 | 技術隨筆

其實本來沒打算寫這篇的,不過之前在寫第二篇: [該如何學好 "寫程式" #2. 為什麼 programmer 該學資料結構 ??] 時,寫的太高興,忘了查 System.Collections.Generics.SortedList 的 KEY 能不能重複... 結果貼出來後同事才提醒我,SortedList 的 KEY 一樣得是唯一的... Orz

 

實在是不想自己再去寫這種資料結構的作業... 一來我寫不會比較好,二來自己寫的沒支援那堆 ICollection, IEnumerable 等 interface 的話,用起來也是很難用... 就到 www.codeplex.com 找了找,沒想到還真的找到一個: NGenerics :D 找到之後才發現挖到寶了,裡面實作的資料結構還真完整,Heap, BinaryTree, CircularQueue, PriorityQueue...  啥的一應俱全,好像看到資料結構課本的解答本一樣 @_@,有興趣研究的人可以抓它的 Source Code 來看..

 

這套 LIB 的實作範圍很廣,除了我前兩篇介紹很基本的那幾個之外,其它連一些數學的跟圖型,甚至是各種排序法的實作都包含在內。要看介紹就到它的官方網站看吧! 很可惜的是它的文件不像 MSDN 一般,有明確的標示時間複雜度... 不過它有附 Source Code, 拼一點的話還是可以自己看程式... 哈哈 :D

 

我就拿 NGenerics 來改寫之前我提供的範例程式吧,那個查通訊錄的程式就不用再改寫了,看不大出來效果差在那。我們來改寫複雜一點的,也就是高速公路的例子。

 

先來看看有什麼東西可以用? NGenerics.DataStructures.General 這個 Namespace 下竟然有現成的 Graph 類別!! 而 NGenerics.Algorithms 下也有現成的 GraphAlgorithm 這演算法的實作... Orz, 裡面提供了三種演算法,光看名字還真搞不懂它是啥... 分別查了一下,是這三個... 找到的都是教授或是考古題之類的網站 ... 咳咳...

  1. Dijkstras Algorithm (代克思托演算法): ... 這種名字難怪我記不住 @_@,這演算法就是我在第三篇提過比較好的演算法,由起點一路擴散出去的作法。
  2. Kruskals Algorithm: 這名字大概太難翻了,沒人把它翻成中文的.. 哈哈,這演算法是找出 minimal spanning tree (最小生成樹),這篇不講教條了,跳過跳過,有興趣自己看 :D
  3. Prims Algorithm (普林演算法): 這名字好記多了... 一樣是找最短路逕 minimal spanning tree 的演算法

來看看原本我寫了上百行的程式 (請參考上一篇),用這個 LIB 改寫有多簡單吧! 先來看看建地圖的部份。Graph<T> 的 T 是指圖的節點型別。暫時不管收費站的問題,因為 GRAPH 的模型裡,只有路逕是有成本的,點本身沒有。直接用 string 來識別點 (vertex),兩個點跟它的距離就當作路段 (edge)。建資料還真有點囉唆,打了不少字:

利用 NGeneric 的 Graph 來建立高速公路的模型[copy code]
            Graph<string> highway = new Graph<string>(false);            highway.AddVertex("基金");            highway.AddVertex("七堵收費站");            highway.AddVertex("汐止系統");            // 以下略            highway.AddEdge(                highway.GetVertex("基金"),                 highway.GetVertex("七堵收費站"),                4.9 - 0);            highway.AddEdge(                highway.GetVertex("七堵收費站"),                 highway.GetVertex("汐止系統"),                 10.9 - 4.9);            // 以下略
   1:  Graph<string> highway = new Graph<string>(false);
   2:  highway.AddVertex("基金");
   3:  highway.AddVertex("七堵收費站");
   4:  highway.AddVertex("汐止系統");
   5:  // 以下略
   6:  highway.AddEdge(
   7:      highway.GetVertex("基金"), 
   8:      highway.GetVertex("七堵收費站"),
   9:      4.9 - 0);
  10:  highway.AddEdge(
  11:      highway.GetVertex("七堵收費站"), 
  12:      highway.GetVertex("汐止系統"), 
  13:      10.9 - 4.9);
  14:  // 以下略

 

都是我一行一行慢慢打的 @_@... 地圖建完後,怎麼找出兩點之間的最短路逕? 只要這段...

找出 [機場端] 到 [基金] 的最短路逕[copy code]
            Graph<string> result = GraphAlgorithms.DijkstrasAlgorithm<string>(                highway,                highway.GetVertex("機場端"));            Console.WriteLine(result.GetVertex("基金").Weight);
   1:  Graph<string> result = GraphAlgorithms.DijkstrasAlgorithm<string>(
   2:      highway,
   3:      highway.GetVertex("機場端"));
   4:  Console.WriteLine(result.GetVertex("基金").Weight);

 

因為每個路段的 weight 我是填上油錢 (一公里兩塊錢),所以印出來的就是兩端要花的油錢。那麼被我們忽略掉的收費站怎麼算? 因為圖型的 MODEL 裡,點是沒有 weight 的,因此我們必需把路段改成有方向的,也就是南下及北上分別算不同的路段 (edge), 同時把過路費加到 weight 裡。

 

這個演算法的實作有個小缺點,它只傳回結果,沒把過程傳回來...,所以我們只能算出要花多少錢,沒有很簡單的方法拿到該怎麼走。不過好在它有附原始碼,需要的人就拿來改一下吧 :D,多傳個 delegate 或是用它定義的 IVisitor 讓它走完所有的點,你就可以取得沿路的資訊了。

 

這篇主要是介紹這個意外發現的LIB,就不深入的挖這些細節了,有興趣的讀者們可以自己試看看,不難的。見識到這類演算法函式庫的威力了嗎? 用起來一點都不難,不過要知道怎麼用還真的要好好研究一下...。整套 NGenerics 都是這類的東西,有興趣的讀者好好研究吧 :D



10/1/2008 4:09:00 AM

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

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

自從貼了上一篇 [該如何學好 "寫程式"] 一文,原本以為這種老生常談的內容沒什麼人會看,沒想到還有人給我回應.. :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/27/2008 11:45:00 PM

MSDN Magazine 十月號竟然下架了...

Microsoft.NET | Threading | 技術隨筆 | MSDN

image

上個禮拜才看完 2008 / 10 的 MSDN Magazine,才想說這期 MSDN 的內容,有一半以上的主題是討論多核心處理器效能問題 (平行處理,多核的 CACHE 機制對程式的影響... etc),不過剛才才想貼篇心得,上網一看竟然又縮回去只剩九月號? @_@

難道我是夢到我看過十月號的嘛? 哈哈... GOOGLE 找一下還真的給我找到庫存網頁 :D 當然要貼來紀念一下。不知道重新上架後內容會不會多幾篇文章?

 

這期的主題還真的都押在多核處理器的效能問題上。光是平行處理 ( TPL, PLINQ ) 的方法就好幾篇:

  • Design Considerations For Parallel Programming
  • Improved Support For Parallelism In The Next Version Of Visual Studio
  • Build Concurrent Apps From Simple F# Expressions

 

另外在探討多核心效能問題的也有幾篇:

  • new Thread(ReadEditorsNote).Start(); yourAttention.WaitOne() //這標題還蠻有意思的...
  • .NET Matters: False Sharing
  • Exploring High-Performance Algorithms

 

不過看完只記得部份重點而以,本來想多寫點心得,不過原文網址都連不到了... 再等幾天吧 :D






精選文章

RUN! PC 文章及範例下載
2010/07. 結合檔案及資料庫的交易處理
2010/05. TxF讓檔案系統也能達到交易控制
2010/04. 生產者 vs 消費者 - 執行緒的供需問題
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