本文章是由機器翻譯。

平行計算

資料處理:並行和性能

Johnson M.Hart

下載代碼示例

處理資料集合是一項基本的計算任務,許多實際問題本質上是並行問題,因此有可能在多核系統上實現更高的性能和輸送量。我將比較幾種截然不同的 Windows 方法,通過高度的資料並行來解決問題。

我用於此項比較的基準測試是搜索問題 (Geonames),該問題來自 Troy Magennis 所著書籍《LINQ to Objects Using C# 4.0》(Addison-Wesley,2010)的第 9 章。備選解決方案包括:

  • 並行語言集成查詢 (PLINQ) 和 C# 4.0,無論是否增強原始代碼。
  • 使用 C 編寫的 Windows 本機代碼、Windows API、執行緒和記憶體映射檔。
  • Windows C#/Microsoft .NET Framework 多執行緒代碼。

可以從我的網站 (jmhartsoftware.com) 獲得所有解決方案的原始程式碼。沒有直接比較其他並行技術,例如 Windows 任務並行庫 (TPL),儘管 PLINQ 是構建在 TPL 基礎上的。

比較和評估備選解決方案

按照重要性排列,解決方案評估標準包括:

  • 按照完成任務所需時間計算的總性能。
  • 在並行度(任務數)、內核數和資料集合規模方面的可擴展性。
  • 代碼簡單性、優美性、易維護性和類似的無形因素。

結果摘要

本文將展示有代表性的基準測試搜索問題的測試結果:

  • 您可以成功利用多核 64 位系統來提高許多資料處理問題中的性能,並且可以在解決方案中採用 PLINQ。
  • 要獲得有競爭力的、可擴展的 PLINQ 性能,需要使用帶索引的資料集合物件;僅僅支援 IEnumerable 介面是不夠的。
  • C#/.NET 和本機代碼解決方案速度最快。
  • 原始 PLINQ 解決方案的速度幾乎要慢 10 倍,並且不能擴展到兩個任務以上,而其他解決方案可以在六核系統(測試的最多內核數)上成功地擴展到 6 個任務。但是,增強代碼可明顯改進原始解決方案。
  • 無論從哪方面來說,PLINQ 代碼都是最簡單、最優美的,因為 LINQ 為駐留在記憶體中的資料和外部資料提供了聲明性查詢功能。本機代碼非常笨拙;C#/.NET 代碼比 PLINQ 要好得多,但是不如後者簡單。
  • 所有方法在檔大小方面都能很好地擴展到測試系統實體記憶體的上限。

基準測試問題:Geonames

本文的想法來自 Magennis 所著的 LINQ 書籍的第 9 章。該章節通過在包含超過 725 萬個地名(即,每 1,000 人就會有 1 個以上的位置)、檔大小達到 825MB 的地理資料庫中進行搜索,從而演示了 PLINQ。每個地名都通過一條 UTF-8 (en.wikipedia.org/wiki/UTF-8) 文本行記錄(可變長度)來表示,其中包含超過 15 個用定位字元分隔的資料列。注意:UTF-8 編碼確保了定位字元 (0x9) 或分行符號 (0xA) 的值不會成為多位元組序列的一部分;這對於部分實施方案極其重要。

Magennis 的 Geonames 程式通過硬編碼的查詢來識別海拔(第 15 列)超過 8,000 米的所有位置,然後顯示地名、國家/地區和海拔(按海拔的降冪排列)。如果您想知道,一共有 16 個這樣的地點,其中珠穆朗瑪峰最高,達到 8,848 米。

Magennis 報告所用的時間為 22.3 秒(單核)和 14.1 秒(雙核)。以前的經驗(例如,請參見我的文章“Windows Parallelism, Fast File Searching and Speculative Processing”,網址為 informit.com/articles/article.aspx?p=1606242)表明,這種大小的檔可以在幾秒鐘內處理完畢,而且性能可隨著內核數量的增加而相應提高。因此,我決定嘗試再現這種經驗,並且還嘗試增強 Magennis 的 PLINQ 代碼,以便獲得更好的性能。最初的 PLINQ 功能增強幾乎使性能翻了一番,但並沒有提高可擴展性;但是經過進一步增強後,性能幾乎與本機代碼和 C# 多執行緒代碼一樣出色。

由於某些原因,此基準測試非常有趣:

  • 主題(地理位置和特性)本身就饒有趣味,而且很容易推廣此查詢。
  • 資料並行度很高;從原理上講,每條記錄都可以併發處理。
  • 按照今天的標準,檔大小很普通,但只需將 Geonames allCountries.txt 檔本身反復連接幾次,就可以測試更大的檔。
  • 處理是有狀態的;有必要確定行和欄位邊界,以便對檔進行分區,而且必須處理行,以便識別各個欄位。

一項假設: 假設識別出來的記錄(在本例中為海拔高於 8,000 米的位置)非常少,因此排序和顯示時間與整個處理(需要檢查每個位元組)時間相比非常少。

另一項假設: 性能結果顯示了處理駐留在記憶體中的資料集合(例如由前一個程式步驟生成的資料)所需的時間。基準測試程式將讀取檔,但是測試程式將運行幾次,以確保該檔駐留在記憶體中。但是,我將提到初次載入檔所需的時間,此時間對所有解決方案幾乎都是一樣的。

性能比較

第一套測試系統是一台六核桌上型電腦系統,其上運行了 Windows 7(AMD Phenom II、2.80 GHz、4GB 記憶體)。隨後,我將展示其他三套系統的結果,這三套系統具有超執行緒 (en.wikipedia.org/wiki/Hyper-threading) 和不同的內核數。

图 1 以所用時間(以秒為單位)和“並行度”(並行任務數,此值可以大於處理器的數量)之間的關係,顯示了六種不同 Geonames 解決方案的結果;測試系統具有六個內核,但實施方案控制的是並行度。六項任務時可達到最優性能;超過六項任務,會導致性能降低。所有測試都使用了原始 Geonames 825MB allCountries.txt 資料檔案。

圖 1 Geonames 性能與並行度之間的關係

實施方案如下(將在後面進行更全面的解釋):

  1. Geonames 原始方案。這是 Magennis 的原始 PLINQ 解決方案。性能不佳,並且不能隨處理器數量進行擴展。
  2. Geonames Helper 方案。這是 Geonames 原始方案 的性能增強版。
  3. Geonames MMChar 方案。此方案嘗試利用類似于 Geonames 執行緒方案 中所用的記憶體映射檔類來增強 Geonames Helper 方案,但並不成功。注意:記憶體映射使檔可以像在記憶體中一樣引用,而無需明顯的 I/O 操作;它也可以提供性能優勢。
  4. Geonames MMByte 方案。此解決方案修改了 MMChar 方案,以便處理輸入檔的各個位元組,而前三種解決方案將 UTF-8 字元轉換為 Unicode(每個為 2 位元組)。此方案是前四種解決方案中性能最佳的一種方案,其性能超過了 Geonames 原始方案 的兩倍。
  5. Geonames 執行緒方案 不使用 PLINQ。這是一種 C#/.NET 實施方案,使用了執行緒和記憶體映射檔。其性能高於索引方案(下一種方案),與本機代碼方案 大致持平。此解決方案與 Geonames 本機代碼方案 提供的並行度可擴展性最佳。
  6. Geonames 索引方案。此 PLINQ 解決方案對資料檔案進行預處理(大約需要 9 秒鐘),創建駐留在記憶體中的 List<byte[]>物件,以供後續 PLINQ 處理使用。預處理的代價可被多次查詢分攤,因此其性能僅僅比 Geonames 本機代碼方案Geonames 執行緒方案 稍差。
  7. Geonames 本機代碼方案圖 1 中未顯示)不使用 PLINQ。這是 C Windows API 實施方案,使用了執行緒和記憶體映射檔,如我寫的書籍《Windows System Programming》(Addison-Wesley,2010)中第 10 章所述。全面的編譯器優化對這些結果很重要;預設的優化方式只能實現大約一半的性能。

所有實施方案都是 64 位版的。32 位版在大多數情況下能夠正常使用,但是當檔更大時就會失敗(請參見圖 2)。图 2 顯示了並行度為 4 和檔更大時的性能。

圖 2 Geonames 性能與檔大小之間的關係

本例中的測試系統具有四個內核(AMD Phenom 四核、2.40 GHz、8GB 記憶體)。通過連接原始檔的多個副本創建了更大的檔。图 2 僅顯示了三種最快的解決方案,包括 Geonames 索引方案—最快的 PLINQ 解決方案(不考慮檔預處理);其檔大小方面的性能可擴展到統實體記憶體的上限。

我現在將介紹第二種到第七種解決方案,並且深入討論 PLINQ 技術。在此之後,我將討論其他測試系統上的結果,並且總結我的發現。

增強的 PLINQ 解決方案:Geonames Helper 方案

图 3 顯示了我在 Geonames Helper 方案 中對 Geonames 原始方案 代碼的更改(顯示為粗體)。

圖 3 Geonames Helper 方案,其中突出顯示了對原始 PLINQ 代碼的更改

class Program
{
  static void Main(string[] args)
  {
    const int nameColumn = 1;
    const int countryColumn = 8;
    const int elevationColumn = 15;

    String inFile = "Data/AllCountries.txt";
    if (args.Length >= 1) inFile = args[0];
        
    int degreeOfParallelism = 1;
    if (args.Length >= 2) degreeOfParallelism = int.Parse(args[1]);
    Console.WriteLine("Geographical data file: {0}.
Degree of Parallelism: {1}.", inFile, degreeOfParallelism);

    var lines = File.ReadLines(Path.Combine(
      Environment.CurrentDirectory, inFile));

    var q = from line in 
      lines.AsParallel().WithDegreeOfParallelism(degreeOfParallelism)
        let elevation = 
          Helper.ExtractIntegerField(line, elevationColumn)
        where elevation > 8000 // elevation in meters
        orderby elevation descending
        select new
        {
          elevation = elevation,
          thisLine = line
         };

    foreach (var x in q)
    {
      if (x != null)
      {
        String[] fields = x.thisLine.Split(new char[] { '\t' });
        Console.WriteLine("{0} ({1}m) - located in {2}",
          fields[nameColumn], fields[elevationColumn], 
          fields[countryColumn]);
      }
    }
  }
}

很多讀者可能對 PLINQ 和 C# 4.0 並不熟悉,因此我將對圖 3 稍作說明,簡單介紹增強功能:

  • 第 9-14 行允許使用者在命令列中指定輸入檔案名和並行度(最大併發任務數);這些值在原始方案中是硬編碼的。
  • 第 16-17 行開始以非同步方式讀取檔行,並且隱式將行的類型設置為 C# String 陣列。這些行的值直到第 19-27 行才會用到。Geonames MMByte 方案 等其他解決方案使用不同的類,這樣的類自己就有 ReadLines 方法,因此只需要修改這些代碼行。
  • 第 19-27 行是 LINQ 代碼以及 PLINQ AsParallel 擴展。這段代碼與 SQL 相似,變數“q”的隱含類型為物件陣列,其中的物件由一個整數海拔和一個 String 組成。請注意,PLINQ 執行所有的執行緒管理工作;AsParallel 方法是將串列 LINQ 代碼轉變成 PLINQ 代碼唯一需要的方法。
  • 第 20 行。图 4 顯示了 Helper.ExtractIntegerField 方法。原始程式在第 33 行中,以類似于用來顯示結果的方式使用 String.Split 方法(圖 3)。這是 Geonames Helper 方案Geonames 原始方案 相比,能夠提高性能的關鍵,因為它不再需要為每一行中的每個欄位分配 String 物件。

圖 4 Geonames Helper 類與 ExtractIntegerField 方法

class Helper
{
  public static int ExtractIntegerField(String line, int fieldNumber)
  {
    int value = 0, iField = 0;
    byte digit;

    // Skip to the specified field number and extract the decimal value.
foreach (char ch in line)
    {
      if (ch == '\t') { iField++; if (iField > fieldNumber) break; }
      else
      {
        if (iField == fieldNumber)
        {
          digit = (byte)(ch - 0x30);  // 0x30 is the character '0'
          if (digit >= 0 && digit <= 9) 
            { value = 10 * value + digit; }
          else // Character not in [0-9].
Reset the value and quit.
{ value = 0; break; }
        }
      }
    }
    return value;
  }
}

請注意,第 19 行中使用的 AsParallel 方法可以處理任何 IEnumerable 物件。 正如我前面所述,圖 4 顯示了 Helper 類的 ExtractIntegerField 方法。 它只是提取並評估指定的欄位(本例中為海拔),避免調用庫方法影響性能。 從圖 1 中可以看出,這種增強使並行度為 1 時的性能翻了一倍。

Geonames MMChar 方案和 Geonames MMByte 方案

Geonames MMChar 方案 使用自訂類 FileMmChar 對輸入檔執行記憶體映射,試圖提高性能,但這種嘗試並不成功。 而 Geonames MMByte 方案 則確實帶來了很大的好處,因為輸入檔的位元組無需擴展為 Unicode。

MMChar 方案 需要一個新的類 FileMmChar,該類支援 IEnumerable<String>介面。 FileMmByte 類與 FileMmChar 類相似,但處理的是 byte[] 物件,而不是 String 物件。 對圖 3 所做的唯一重要的代碼更改是第 16-17 行,這兩行現在為:

var lines = FileMmByte.ReadLines(Path.Combine(
    Environment.CurrentDirectory, inFile));

代碼

public static IEnumerable<byte[]> ReadLines(String path)

在 FileMmByte 中支援 IEnumerable<byte[]>介面,將構造一個 FileMmByte 物件和一個 IEnumerator<byte[]>物件,後者用於從映射的檔中掃描各個行。

請注意,FileMmChar 和 FileMmByte 類都是“不安全”的,因為它們會創建和使用指標來訪問檔,而且它們會交互使用 C#/本機代碼。但是,所有的指標使用都隔離在單獨的程式集中,並且代碼中使用陣列而不是指標解除引用。.NET Framework 4 MemoryMappedFile 類在此毫無説明,因為它必須使用訪問器函數,才能從映射的記憶體中移動資料。

Geonames 本機代碼方案

Geonames 本機代碼方案 使用了 Windows API、執行緒和檔記憶體映射。《Windows System Programming》中的第 10 章介紹了基本代碼模式。該程式必須直接管理執行緒,還必須小心地將檔映射到記憶體。其性能大大高於所有 PLINQ 實施方案,但 Geonames 索引方案 除外。

但是,在 Geonames 問題與簡單的無狀態檔搜索或變換之間有一個重要區別。面臨的挑戰是如何確定對輸入資料進行分區 的正確方法,以便為不同的任務分配不同的分區。沒有什麼顯而易見的方法可以在不用掃描整個檔的情況下確定行邊界,因此為每個任務分配固定大小的分區並不可行。但是,在並行度為 4 時,該解決方案非常直觀:

  • 將輸入檔分成四個相等的分區,並且將分區的開始位置作為執行緒函數的參數通知每個執行緒。
  • 然後,讓每個執行緒在該分區中開始 的所有行(記錄)。這意味著執行緒可能會掃描到下一個分區中,以便完成對該分區中開始的最後一行的處理。

Geonames 執行緒方案

Geonames 執行緒方案 使用的邏輯與 Geonames 本機代碼方案 相同;事實上,有些代碼完全相同或基本一樣。但是,lambda 運算式、擴展方法、容器和其他 C#/.NET 功能則大大簡化了代碼的編寫。

就像 MMByte 方案MMChar 方案 一樣,檔記憶體映射需要使用“不安全”的類,並且需要交互使用 C#/本機代碼,以便使用指向映射記憶體的指標。但所做的工作是值得的,因為 Geonames 執行緒方案 的性能與 Geonames 本機代碼方案 相同,而代碼要簡單得多。

Geonames 索引方案

本機代碼方案.NET 執行緒方案 的結果相比,PLINQ 結果(原始方案Helper 方案MMChar 方案MMByte 方案)均不能讓人滿意。那有沒有辦法,既利用 PLINQ 的簡單性和優美性,又不至於犧牲性能?

儘管不可能準確知道 PLINQ 如何處理查詢(圖 3 中的第 16-27 行),但是 PLINQ 很可能沒有什麼好辦法對輸入行進行分區,以供各個任務並行處理。認為分區可能是 PLINQ 性能問題的起因,這是一種可行的假設。

從 Magennis 所著的書籍中(第276-279 頁),行的 String 陣列支援 IEnumerable<String>介面(另請參見 John Sharp 所著的書籍《Microsoft Visual C# 2010 Step by Step》[Microsoft Press,2010],第 19 章)。但是,行沒有編制索引,因此 PLINQ 可能使用“大塊分區”方法。而且,FileMmChar 和 FileMmByte 類的 IEnumerator.MoveNext 方法速度很慢,因為它們需要掃描每個字元,直到下一行所在的位置為止。

如果為行的 String 陣列編制索引會發生什麼情況呢?我們是不是能提高 PLINQ 的性能,尤其是在通過對輸入檔執行記憶體映射之後?Geonames 索引方案 將此技術產生的結果與本機代碼相比,表明此技術確實能提高性能。但是,一般來說,這種技術要麼需要先付出代價將各個行移到記憶體中的清單或陣列中,而清單或陣列則已經編制索引(這種代價可分攤到隨後執行的多個查詢);要麼檔或其他資料來源已經編制好索引(可能是在前一個程式步驟中生成的),從而消除了預處理代價。

先期的編制索引操作非常簡單;只需要依次訪問每一行,然後將其添加到清單中即可。在第 16-17 行中使用清單物件(如圖 3 所示),並且在以下程式碼片段中也使用清單物件,該程式碼片段演示了預處理:

// Preprocess the file to create a list of byte[] lines
List<byte[]> lineListByte = new List<byte[]>();
var lines = 
    FileMmByte.ReadLines(Path.Combine(Environment.CurrentDirectory, inFile));
// ...
Multiple queries can use lineListByte
// ....
foreach (byte[] line in lines) { lineListByte.Add(line); }
// ....
var q = from line in lineListByte.AsParallel().
WithDegreeOfParallelism(degreeOfParallelism)

請注意,將清單轉換為數組,能夠稍稍提高資料處理速度,但會增加預處理時間。

最後的性能增強

Geonames 索引方案 的性能還可以進一步提高,只需為每一行中的各個欄位編制索引即可。因為這樣可使 ExtractIntegerField 方法不需要掃描一行中的所有字元,就能找出指定的欄位。

Geonames IndexFields 實施方案修改了 ReadLines 方法,使其返回的行為一個物件,而該物件包含一個 byte[] 陣列以及一個包含每個欄位的位置資訊的 uint[] 陣列。此方案與 Geonames 索引方案 相比,性能大約能提高 33%,此性能已經相當接近本機代碼和 C#/.NET 解決方案的水準。(Geonames IndexFields 方案 包含在代碼下載中。) 而且現在也更容易構建更通用的查詢,因為各個欄位都已經可以直接使用。

局限性

有效的解決方案都要求資料駐留在記憶體中,而性能優勢並不能擴展到超大型的資料集合。本例中的“超大型”是指接近系統實體記憶體大小的資料規模。在 Geonames 示例中,擁有 8GB 記憶體的測試系統可以處理 3,302MB 大小的檔(原始檔的四個副本)。我對接起來的八個原始檔副本進行了測試,但所有解決方案的速度都非常慢。

正如前文所述,如果資料檔案處於“活動”狀態,也就是說資料檔案最近被訪問過並且很可能在記憶體中,性能也會達到最佳。在初次運行過程中對資料檔案進行分頁可能需要 10 秒或更長時間,與上述程式碼片段中的編制索引操作大致相當。

總而言之,本文中的結果適用于駐留在記憶體中的資料結構,而今天的記憶體大小和價格也允許非常大的資料物件(例如包含 725 萬個地名的檔)駐留在記憶體中。

其他測試系統結果

图 5 顯示了其他系統(Intel i7 860、2.80GHz、四核、八執行緒、Windows 7、4GB 記憶體)上的測試結果。處理器支援超執行緒,因此測試的並行度值為 1、2、……、8。图 1 基於六核 AMD 測試系統;此系統不支援超執行緒。

圖 5 Intel i7 860、2.80GHz、四核、八執行緒、Windows 7、4GB 記憶體

其他兩種測試配置產生的結果都差不多(我的網站上提供了全面的資料):

  • Intel i7 720、1.60GHz、四核、八執行緒、Windows 7、8GB 記憶體
  • Intel i3 530、2.93GHz、雙核、四執行緒、Windows XP64、4GB 記憶體

有趣的性能特徵包括:

  • Geonames 執行緒方案Geonames 本機代碼方案 提供的性能總是最佳的。
  • Geonames 索引方案 是最快的 PLINQ 解決方案,其性能接近 Geonames 執行緒方案注意:GeonamesIndexFields 方案要稍微快一點,但未顯示在圖 5中。
  • 除了 Geonames 索引方案 以外,當並行度大於 2 時,所有 PLINQ 解決方案的性能都會隨著並行度的提高而降低;也就是說,性能會隨著並行任務數量的增加而降低。通過這個例子可以看出,PLINQ 只能對已編制索引的物件產生不錯的性能。
  • 超執行緒對性能的貢獻微不足道。因此,對於並行度大於 4 的情況,Geonames 執行緒方案Geonames 索引方案 的性能並沒有明顯提高。這種較差的超執行緒擴展性的起因可能是由於將兩個執行緒安排在同一個內核的邏輯處理器上,而不能確保這兩個執行緒盡可能運行在不同的內核上。但是,這種解釋似乎說不通,因為 Mark E.Russinovich、David A.Solomon 和 Alex Ionescu 在他們所著的書籍《Windows Internals, Fifth Edition》(Microsoft Press,2009)的第40 頁上說道:在安排計畫時,物理處理器優先于邏輯處理器。對於執行緒方案本機代碼方案索引方案,當並行度超過 4 時,與並行度為 1 時的結果相比,不具備超執行緒功能的 AMD 系統(圖 1)提供的性能可達到三到四倍。图 1 表明當並行度與內核數量相同時,性能最佳,此時多執行緒性能可達到並行度為 1 時的 4.2 倍。

結果摘要

PLINQ 提供了一種優秀的模型,用於處理記憶體中的資料結構,並且只需實現一些很小的改動(例如 Helper 方案)或更先進的技巧(如 MMByte 方案 所示),就能提高現有代碼的性能。但是,任何一種簡單的增強都不可能達到與本機代碼或多執行緒 C#/.NET 代碼相當的性能。而且,這樣的增強不能與內核數量和並行度同步提升。

PLINQ 可以實現與本機代碼和 C#/.NET 代碼接近的性能,但它需要使用編制好索引的資料物件。

使用代碼和資料

我的網站 (jmhartsoftware.com/SequentialFileProcessingSupport.html) 上提供了所有代碼,請按照下麵的說明進行操作:

  • 訪問下載頁面,下載包含 PLINQ 代碼和 Geonames 執行緒方案 代碼的 ZIP 檔。所有 PLINQ 變化形式均包含在 GeonamesPLINQ 專案中(Visual Studio 2010;Visual Studio 2010 Express 就夠用了)。Geonames執行緒方案 包含在 GeonamesThreads Visual Studio 2010 專案中。這些專案都是針對 64 位版進行配置的。該 ZIP 檔還包含一個試算表,其中包含圖 1、2 和 5 中使用的原始性能資料。該檔的開頭包含簡單的“用法”說明,解釋了用於選擇輸入檔、並行度和實施方案的命令列選項。
  • 訪問 Windows System Programming 支援頁 (jmhartsoftware.com/comments_updates.html),以下載 Geonames 本機代碼方案 的代碼和專案(一個 ZIP 檔),您可以在此找到 Geonames 專案。ReadMe.txt 檔解釋了其結構。
  • download.geonames.org/export/dump/allCountries.zip 下載 GeoNames 資料庫。

未探討的問題

本文比較了幾種備用技術的性能,這些技術均用於解決同樣的問題。具體的方法是按照說明使用這些技術的標準介面,並且假設處理器和執行緒採用一種簡單的共用記憶體模式。但是,深入研究底層實施方案或測試電腦的特定功能沒有什麼明顯的效果,而且有大量問題都可以在未來進行探討。示例如下:

  • 如果緩存未命中,會有何影響?有辦法降低其影響嗎?
  • 固態磁片有何影響?
  • 有辦法縮小 PLINQ 索引方案執行緒方案本機代碼方案 之間的性能差距嗎?減少 FileMmByte IEnumerator.MoveNext 和 Current 方法中複製的資料量,這樣的實驗並未顯示出任何明顯的好處。
  • 性能是否已經接近由記憶體頻寬、CPU 速度和其他體系結構特徵所決定的最大值?
  • 有辦法讓超執行緒系統(請參見圖 5)像不具備超執行緒的系統(圖 1)一樣實現可擴展的性能嗎?
  • 您可以使用分析工具和 Visual Studio 2010 工具來識別和去除性能瓶頸嗎?

我希望您能夠深入研究。

Johnson (John) M. Hart 是一位顧問,專門從事 Microsoft Windows、Microsoft .NET Framework 應用程式體系結構和開發、技術培訓以及技術寫作。 他在 Cilk Arts Inc.(自從被 Intel Corp.收購以來)、Sierra Atlantic Inc.、Hewlett-Packard Co.和 Apollo Computer 等公司擁有多年的軟體工程師、工程經理和架構師經驗。他成為電腦科學專家已經有很多年了,並且撰寫了四個版本的《Windows System Programming》(Addison-Wesley,2010)。

衷心感謝以下技術專家對本文的審閱:Michael BruestleAndrew Greenwald, Troy Magennis and CK Park