000 Windows 與 C++ 中的同步處理發展

Post date: 2012/12/20 上午 01:56:13

Kenny Kerr

http://msdn.microsoft.com/zh-tw/magazine/jj721588.aspx

當我第一次開始寫併發軟體時,c + + 已不支援同步。 Windows 本身了只有少數的同步基元,所有這些都在內核中實施。 我傾向使用關鍵區段,除非跨進程,同步所需在這種情況下我使用互斥體。 總體而言,這兩個鎖,或鎖定的物件。

互斥體採用其名稱從"相互排斥,"同步的另一個名字的概念。 它是指只有一個執行緒可以在一次訪問某些資源的保障。 臨界區從實際可能會訪問此類資源的代碼部分採用其名稱。 為確保正確性,只有一個執行緒可以一次執行此代碼的臨界區。 這兩個鎖定物件具有不同的功能,但卻不只是要記住他們是鎖定、 他們都提供相互排斥的保證和既可以用來標定代碼的關鍵區段。

今天同步景觀發生了巨大變化。 有過多的 c + + 程式師的選擇。 Windows 現在支援更多的同步功能,和 c + + 本身終於提供併發和同步功能的有趣集合對於那些使用編譯器支援 C + + 11 標準。

在本月的專欄我將探索在 Windows 和 c + + 中的同步的狀態。 我開始審查由 Windows 本身提供的同步基元,然後再考慮提供的標準 c + + 庫的替代品。 如果您主要關心的可攜性,新的 c + + 庫添加內容將會非常吸引人。 如果,然而,可攜性較少關注和性能極其重要,然後讓你熟悉什麼 Windows 現在提供了將重要。 讓我們深入右中。

重要區段

第一次最多是關鍵節的物件。 這鎖由無數的應用程式使用的使用量,但已有骯髒的歷史。 當我第一次開始使用的關鍵區段時,她們真的很簡單。 若要創建這種鎖,所有你需要是分配 CRITICAL_SECTION 結構並調用 InitializeCriticalSection 函數,以準備使用它。 此函數不傳回值,這意味著它不能失敗。 回來在那些日子裡,不過,有必要為此函數來創建各種系統資源,尤其是內核事件物件,並有可能在極低記憶體的情況下這將失敗,導致的引發的結構化異常。 儘管如此,這是相當罕見,因此大多數開發人員忽略這種可能性。

COM 的普及,與關鍵節的使用暴漲因為許多 COM 類用於關鍵節進行同步,但在許多情況下是沒有實際的爭用的說話很少。 多處理器電腦變得更加普遍,關鍵節內部事件看見更少使用因為關鍵節簡要地將等待獲取鎖的同時旋轉在使用者模式下。 小旋轉計數,意味著許多短暫時期的爭用可能避免內核過渡,大大提高性能。

在這段時間一些內核開發者意識到他們可以大大提高 Windows 的可擴充性,是否他們推遲臨界區事件物件的創建,直到有足夠的爭用,要他們的存在。 這似乎像個好主意,直至開發人員實現,這意味著雖然 InitializeCriticalSection 現在可能不可能會失敗,EnterCriticalSection 函數,該函數 (用於等待鎖擁有權) 已不再可靠。 這可不一樣輕鬆地忽視由開發人員,因為它介紹了各種將已經取得關鍵節不可能以正確使用和破壞無數的應用程式的故障條件。 仍然,可擴充性 wins 不能被忽視。

內核開發人員終於在解決方案中的一個新的和無證,內核事件物件稱為鍵控的事件形式。 你可以讀起來有點有關它在書中,"Windows 內核,"由 Mark E. David A.Russinovich 所羅門和約內斯庫亞曆克斯 (微軟出版社,2012年),但基本上,而不是為每個關鍵節要求的事件物件,一個單一的鍵控的事件可用於所有關鍵節在系統中。 這樣做是因為鍵控的事件物件只是:它依靠的是只是一個指標大小識別碼,自然是位址空間的關鍵地方。

肯定是更新關鍵節使用鍵控的事件完全是一種誘惑,但是如果內核未能分配一個定期事件物件因為許多調試器和其他工具依賴關鍵節的內部結構,鍵控的事件只使用作為最後的手段。

這聽起來可能像很多不相干的歷史但 Windows Vista 開發週期中,大大改進了性能的鍵控事件的事實,這導致引入一個全新的鎖定物件,是更簡單和更快 — — 但,在一分鐘的時間更多。

關鍵節物件現時豁免低記憶體故障,它真的是非常簡單的使用。 圖 1 提供了一個簡單的包裝。

圖 1 的關鍵節鎖

  1. class lock
  2. {
  3. CRITICAL_SECTION h;
  4. lock(lock const &);
  5. lock const & operator=(lock const &);
  6. public:
  7. lock()
  8. {
  9. InitializeCriticalSection(&h);
  10. }
  11. ~lock()
  12. {
  13. DeleteCriticalSection(&h);
  14. }
  15. void enter()
  16. {
  17. EnterCriticalSection(&h);
  18. }
  19. bool try_enter()
  20. {
  21. return 0 != TryEnterCriticalSection(&h);
  22. }
  23. void exit()
  24. {
  25. LeaveCriticalSection(&h);
  26. }
  27. CRITICAL_SECTION * handle()
  28. {
  29. return &h;
  30. }
  31. };

我已經提到的 EnterCriticalSection 函數被輔以一個 TryEnterCriticalSection 函數,提供無阻塞的替代方案。 LeaveCriticalSection 函數釋放鎖,並 DeleteCriticalSection 釋放任何可能已撥出沿途的內核資源。

這樣的關鍵區段是一個合理的選擇。 它執行相當好,它會嘗試避免內核轉換和資源配置。 它仍有點由於其歷史和應用程式的相容性,它必須執行的行李。

互斥鎖

Mutex 物件是一個真正的內核同步物件。 與關鍵章節不同互斥鎖總是會消耗內核分配資源。 好處,當然,是內核然後就能夠提供跨進程同步由於鎖的認識。 作為內核對象,它提供了常用的屬性 — — 如名稱 — —,可以用於從其他進程打開的物件或只是確定在調試器中的鎖定。 您還可以指定存取遮罩來限制對該物件的訪問。 作為一個 intraprocess 的鎖,它的矯枉過正,稍微複雜的使用和慢好多。 圖 2 為未命名的互斥體是有效過程本地提供一個簡單的包裝。

圖 2 互斥鎖

  1. #ifdef _DEBUG
  2. #include <crtdbg.h>
  3. #define ASSERT(expression) _ASSERTE(expression)
  4. #define VERIFY(expression) ASSERT(expression)
  5. #define VERIFY_(expected, expression) ASSERT(expected == expression)
  6. #else
  7. #define ASSERT(expression) ((void)0)
  8. #define VERIFY(expression) (expression)
  9. #define VERIFY_(expected, expression) (expression)
  10. #endif
  11. class lock
  12. {
  13. HANDLE h;
  14. lock(lock const &);
  15. lock const & operator=(lock const &);
  16. public:
  17. lock() :
  18. h(CreateMutex(nullptr, false, nullptr))
  19. {
  20. ASSERT(h);
  21. }
  22. ~lock()
  23. {
  24. VERIFY(CloseHandle(h));
  25. }
  26. void enter()
  27. {
  28. VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  29. }
  30. bool try_enter()
  31. {
  32. return WAIT_OBJECT_0 == WaitForSingleObject(h, 0);
  33. }
  34. void exit()
  35. {
  36. VERIFY(ReleaseMutex(h));
  37. }
  38. HANDLE handle()
  39. {
  40. return h;
  41. }
  42. };

CreateMutex 函數創建鎖,並共同的 CloseHandle 函數關閉進程處理,其中有效鎖的引用計數在內核中的遞減。 等待鎖擁有權被通過普通用途的 WaitForSingleObject 函數,它檢查,並可以選擇等待各種內核對象的終止狀態。 其第二個參數,指示在等待獲取鎖定時阻止調用執行緒應多長時間。 無限的常數是 — — 不奇怪 — — 無限期等待,雖然零值可以防止執行緒在所有等待和將只獲取鎖,如果是免費的。 最後,ReleaseMutex 函數釋放鎖。

互斥鎖是大錘大量電能,但它也要付出代價的性能和複雜性。 在包裝圖 2 散落著斷言來指示可能的方法它可能會失敗,但它是取消資格互斥鎖在大多數情況下的性能影響。

活動

我談到高性能的鎖之前,我需要引入一個更多內核同步物件,我已經提到的其中一個。 雖然實際上並沒有一個鎖,事件物件,因為它不能提供一個直接實現相互排斥的設施,是非常重要的工作執行緒之間的協調。 事實上,它是由關鍵節鎖在內部使用的同一物件 — — 此外,它進來方便高效、 可擴展的方式實現所有類型的併發模式時。

CreateEvent 函數創建事件和 — — 喜歡互斥體 — — CloseHandle 功能關閉,釋放內核對象的控制碼。 因為它不是實際的鎖,它有沒有獲得/釋放語義。 它是由許多內核對象提供的信號傳遞功能的化身。 要瞭解如何信號的作品,你需要欣賞事件物件可以創建在兩個國家之一。 如果您對 CreateEvent 的第二個參數傳遞,然後生成的事件物件是據說是手動重置事件 ; 否則創建自動重置事件。 手動重置事件需要您手動設置和重置該物件的終止的狀態。 為此目的提供的 SetEvent 和 ResetEvent 的功能。 自動重置事件自動­◆ 重置 (更改從終止向受阻) 當釋放等待中的執行緒。 所以自動重置事件非常有用的當一個執行緒需要協調與一個其他執行緒,而手動重置事件非常有用的當一個執行緒需要協調與任意數目的執行緒。 自動重置事件調用 SetEvent 將釋放最一個執行緒,而與手動重置事件的調用將釋放所有等待中的執行緒。 像互斥體,等待事件變為終止狀態是 WaitForSingleObject 函數的情況下實現的。 圖 3 提供一個簡單的包裝,為未命名的事件,可以在任一模式中構造。

圖 3 事件信號

  1. class event
  2. {
  3. HANDLE h;
  4. event(event const &);
  5. event const & operator=(event const &);
  6. public:
  7. explicit event(bool manual = false) :
  8. h(CreateEvent(nullptr, manual, false, nullptr))
  9. {
  10. ASSERT(h);
  11. }
  12. ~event()
  13. {
  14. VERIFY(CloseHandle(h));
  15. }
  16. void set()
  17. {
  18. VERIFY(SetEvent(h));
  19. }
  20. void clear()
  21. {
  22. VERIFY(ResetEvent(h));
  23. }
  24. void wait()
  25. {
  26. VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  27. }
  28. };

輕型讀取器/寫入器鎖定

斯利姆讀寫器 (SRW) 鎖的名稱可能是一口,但重要的詞是"減肥"。程式師可能會忽略此鎖,因為它能夠區分共用的讀者和專屬的作家,或許以為這大材小用,當他們需要的只是關鍵的一段。 事實證明,這是最簡單的鎖,以處理和還到目前為止最快,你當然不需要有共用的讀者才能使用它。 它有此迅速的聲譽,不只是因為它依賴高效鍵控的事件物件,而且還因為它大多是實施在使用者模式下,僅回退到內核如果爭用的執行緒會更好睡。 再次,關鍵節和 mutex 物件提供附加功能可能需要的情況下,如遞迴或進程間的鎖,但往往不你需要的一切是快速和羽量級的鎖,供內部使用。

這種鎖完全依賴我之前,提到的鍵控事件,這種極羽量級儘管提供了大量的功能。 SRW 鎖需要只有指標-­大小的存儲,通過調用進程,而不是內核分配量。 出於此原因,初始化函數,InitializeSRWLock,不能失敗,只是確保鎖在使用之前包含相應的位模式。

等待鎖擁有權實現使用任一獲取­SRWLockExclusive 函數的所謂編寫器鎖或使用 AcquireSRWLockShared 函數讀取器鎖。 但是,更適當的獨佔性和共用的術語。 有相應版本並嘗試獲取函數,是希望為這兩個獨佔並共用模式。 圖 4 為獨佔模式 SRW 鎖提供一個簡單的包裝。 它不會為您要添加的共用模式功能,如果需要硬。 但是,請注意是沒有析構函數,因為沒有要釋放的資源。

圖 4 SRW 鎖

  1. class lock
  2. {
  3. SRWLOCK h;
  4. lock(lock const &);
  5. lock const & operator=(lock const &);
  6. public:
  7. lock()
  8. {
  9. InitializeSRWLock(&h);
  10. }
  11. void enter()
  12. {
  13. AcquireSRWLockExclusive(&h);
  14. }
  15. bool try_enter()
  16. {
  17. return 0 != TryAcquireSRWLockExclusive(&h);
  18. }
  19. void exit()
  20. {
  21. ReleaseSRWLockExclusive(&h);
  22. }
  23. SRWLOCK * handle()
  24. {
  25. return &h;
  26. }
  27. };

條件變數

我需要介紹的最後同步物件是條件變數。 這也許是大多數程式師將會與不熟悉的人。 我,不過,注意條件變數的興趣在最近幾個月。 這可能會是與 C + + 11,但是這個想法不是新的並對這一概念流傳了一段時間在 Windows 上的支援。 事實上,Microsoft.NET 框架以來一直支援條件變數模式最早的版本中,儘管它已合併到顯示器類中,限制它在某些方面的作用。 但此新的興趣也是由於允許條件變數將推出的 Windows Vista 中,令人驚歎的鍵控事件,他們才有改善以來。 雖然條件變數是只是一個併發模式,因此,可以與其他基元執行,在作業系統中的將其列入意味著它可以實現驚人的性能,並釋放,程式師無需確保此類代碼的正確性。 事實上,如果你正在雇用 OS 同步基元,是幾乎不可能確保沒有作業系統本身的説明一些併發模式的正確性。

條件變數模式是很常見的如果你想想看。 程式需要等待才可應滿足一些條件。 評估此條件涉及獲取鎖,評估一些共用的狀態。 如果,然而,尚未滿足還沒條件,必須釋放鎖允許一些其他執行緒來滿足該條件。 評價的執行緒必須等待,再一次獲取鎖之前滿足的條件。 一旦重新獲取鎖,是條件必須重新評估避免明顯競爭條件。 執行本很難似乎因為事實上,有很多其他的陷阱需要擔心 — — 和實施有效的方式將更加困難。 下面的偽代碼闡釋了這一問題:

  1. lock-enter
  2. while (!condition-eval)
  3. {
  4. lock-exit
  5. condition-wait
  6. lock-enter
  7. }
  8. // Do interesting stuff here
  9. lock-exit

但即使在此圖中是一個微妙的 bug。 才能正常工作,必須對等條件之前退出該鎖,但這樣做不會工作因為鎖將永遠不會再被釋放。 以原子方式釋放一個物件和等待另一個的能力至關重要 Windows 提供了要做如此的某些內核對象的 SignalObjectAndWait 函數。 但因為 SRW 鎖住大多是在使用者模式下,需要一個不同的解決方案。 輸入條件變數。

像 SRW 鎖,條件變數占地只有單一指標大小的存儲量,使用故障保護的 InitializeConditionVariable 函數初始化。 如用 SRW 鎖,沒有資源要釋放,因此不再需要的條件變數時只是可以回收的記憶體。

因為本身的條件是特定于程式的它由調用方寫一段作為模式正在單個調用 SleepConditionVariableSRW 函數的身體迴圈。 此函數以原子方式等以醒,一旦滿足條件時釋放 SRW 鎖。 還有一個相應的 SleepConditionVariableCS 函數如果您想要使用條件變數與關鍵節鎖相反。

WakeConditionVariable 函式呼叫來喚醒單一的等待,或睡覺的時候,執行緒。 在返回之前,倭的執行緒將重新獲取鎖。 或者,可以使用 WakeAllConditionVariable 函數來喚醒所有等待的執行緒。 圖 5提供一個簡單的包裝,有必要的 while 迴圈。 請注意有可能無法預知醒睡執行緒並且 while 迴圈可以確保條件始終重新檢查後重新獲取該鎖的執行緒。 它也是重要的是要注意始終計算該謂詞時同時鎖住。

圖 5 的條件變數

  1. class condition_variable
  2. {
  3. CONDITION_VARIABLE h;
  4. condition_variable(condition_variable const &);
  5. condition_variable const & operator=(condition_variable const &);
  6. public:
  7. condition_variable()
  8. {
  9. InitializeConditionVariable(&h);
  10. }
  11. template <typename T>
  12. void wait_while(lock & x, T predicate)
  13. {
  14. while (predicate())
  15. {
  16. VERIFY(SleepConditionVariableSRW(&h, x.handle(), INFINITE, 0));
  17. }
  18. }
  19. void wake_one()
  20. {
  21. WakeConditionVariable(&h);
  22. }
  23. void wake_all()
  24. {
  25. WakeAllConditionVariable(&h);
  26. }
  27. };

阻塞佇列

為此一些形狀,我將作為示例使用阻斷佇列。 讓我強調我不建議一般阻塞佇列。 您可能較好,使用 I/O 完成埠或 Windows 執行緒池,其中超過前者或甚至併發運行 concurrent_queue 類是一個抽象的概念。 任何非阻塞是一般首選。 仍然,阻塞佇列是一個簡單的概念來把握和許多開發商似乎找到有用的東西。 無可否認,不是每個程式需要縮放,但每個程式需要是正確的。 阻斷佇列還提供了充分的機會聘請的正確性同步 — — 和,當然,充分機會搞錯了。

應考慮實施與剛剛鎖和事件的阻斷佇列。 鎖保護共用的佇列和事件信號給消費者生產者已經推到佇列上的東西。 圖 6 提供了一個簡單的例子,使用自動重置事件。 我使用此事件模式,因為 push 方法佇列只有一個元素,並因此,我只想要一個消費者要吵醒,其彈出該佇列。 Push 方法獲取該鎖、 佇列元素,然後信號要喚醒任何等待消費者的事件。 流行的方法獲取該鎖,然後等待,直到佇列不為空之前出列元素並將其返回。 這兩種方法使用 lock_block 類。 為簡潔起見,它還沒被列入,但它只需調用該鎖在其建構函式和退出方法在其析構函數中輸入方法。

圖 6 自動重置阻塞佇列

  1. template <typename T>
  2. class blocking_queue
  3. {
  4. std::deque<T> q;
  5. lock x;
  6. event e;
  7. blocking_queue(blocking_queue const &);
  8. blocking_queue const & operator=(blocking_queue const &);
  9. public:
  10. blocking_queue()
  11. {
  12. }
  13. void push(T const & value)
  14. {
  15. lock_block block(x);
  16. q.push_back(value);
  17. e.set();
  18. }
  19. T pop()
  20. {
  21. lock_block block(x);
  22. while (q.empty())
  23. {
  24. x.exit(); e.wait(); // Bug!
  25. x.enter();
  26. }
  27. T v = q.front();
  28. q.pop_front();
  29. return v;
  30. }
  31. };

但是,注意可能鎖死,因為退出和等待調用不是原子。 如果互斥鎖,我可以使用 SignalObjectAndWait 函數,但阻塞佇列的性能會受到影響。

另一個選項是使用手動重置事件。 信號轉導只要排隊一個元素,而只需定義兩個國家。 事件可以為終止狀態,只要有元素在佇列中,並無信號時它是空的。 這還將執行很多更好地因為有少到內核的調用來發出事件信號工作。 圖 7 提供了這樣一個示例。 請注意如何 push 方法設置的事件,如果佇列有一個元素。 這樣可以避免不必要調用 SetEvent 函數。 流行的方法盡職盡責地清除事件,如果它發現佇列為空。 只要有多個排隊的元素,任意數量的消費者可以彈出關閉佇列元素而不涉及該事件物件,從而提高了可擴充性。

圖 7 手動重置阻塞佇列

  1. template <typename T>
  2. class blocking_queue
  3. {
  4. std::deque<T> q;
  5. lock x;
  6. event e;
  7. blocking_queue(blocking_queue const &);
  8. blocking_queue const & operator=(blocking_queue const &);
  9. public:
  10. blocking_queue() :
  11. e(true) // manual
  12. {
  13. }
  14. void push(T const & value)
  15. {
  16. lock_block block(x);
  17. q.push_back(value);
  18. if (1 == q.size())
  19. {
  20. e.set();
  21. }
  22. }
  23. T pop()
  24. {
  25. lock_block block(x);
  26. while (q.empty())
  27. {
  28. x.exit();
  29. e.wait();
  30. x.enter();
  31. }
  32. T v = q.front();
  33. q.pop_front();
  34. if (q.empty())
  35. {
  36. e.clear();
  37. }
  38. return v;
  39. }
  40. };

在這種情況下沒有潛在的鎖死退出等待輸入序列中另一個消費者無法竊取該事件,因為考慮到它是一個手動重置事件。 很難打敗,在性能方面。 不過,另一種方法 (和或許更自然) 的解決方案是使用條件變數而不是一個事件。 這與 condition_variable 類中輕鬆地完成圖 5 ,它類似于手動重置阻斷佇列,雖然它是簡單些。 圖 8 提供了一個示例。 請注意如何的語義和併發的意圖更加清楚所雇用人數更高級的同步物件。 這種明確有助於避免經常困擾著更多晦澀的代碼的併發 bug。

圖 8 條件變數阻塞佇列

  1. template <typename T>
  2. class blocking_queue
  3. {
  4. std::deque<T> q;
  5. lock x;
  6. condition_variable cv;
  7. blocking_queue(blocking_queue const &);
  8. blocking_queue const & operator=(blocking_queue const &);
  9. public:
  10. blocking_queue()
  11. {
  12. }
  13. void push(T const & value)
  14. {
  15. lock_block block(x);
  16. q.push_back(value);
  17. cv.wake_one();
  18. }
  19. T pop()
  20. {
  21. lock_block block(x);
  22. cv.wait_while(x, [&]()
  23. {
  24. return q.empty();
  25. });
  26. T v = q.front();
  27. q.pop_front();
  28. return v;
  29. }
  30. };

最後,我應該提及,C + + 11 現在提供了一把鎖,稱為互斥體,以及 condition_variable。 C + + 11 互斥體與 Windows 互斥體無關。 同樣,C + + 11 condition_variable 並不基於 Windows 的條件變數。 這是在可攜性方面的好消息。 可以使用任何地方都可以找到符合的 c + + 編譯器。 另一方面,C + + 11 實現在 Visual c + + 2012年版本中執行很糟相比,Windows SRW 鎖和條件變數。 圖 9 提供一個示例與標準 C 執行阻斷佇列 + + 11 的庫類型。

圖 9 + + 11 阻塞佇列

  1. template <typename T>
  2. class blocking_queue
  3. {
  4. std::deque<T> q;
  5. std::mutex x;
  6. std::condition_variable cv;
  7. blocking_queue(blocking_queue const &);
  8. blocking_queue const & operator=(blocking_queue const &);
  9. public:
  10. blocking_queue()
  11. {
  12. }
  13. void push(T const & value)
  14. {
  15. std::lock_guard<std::mutex> lock(x);
  16. q.push_back(value);
  17. cv.
  18. notify_one();
  19. }
  20. T pop()
  21. {
  22. std::unique_lock<std::mutex> lock(x);
  23. cv.wait(lock, [&]()
  24. {
  25. return !q.empty();
  26. });
  27. T v = q.front();
  28. q.pop_front();
  29. return v;
  30. }
  31. };

作為一般會併發磁帶庫的支援,在時間,無疑將改善標準 c + + 庫執行。 C + + 委員會採取了一些小型的、 保守的步驟,對併發性支援,應該承認,但工作尚未完成。 討論我最後三個列中時,未來的 c + + 併發是仍然問題。 現在,在 Windows 和先進的 c + + 編譯器一些優秀同步基元的結合,使生產輕量和可擴充性的併發安全程式令人信服的工具組。

肯尼 · 克爾 是充滿熱情的本機 Windows 開發的軟體工匠。他在聯繫 kennykerr.ca

由於以下的技術專家對本文的審閱:穆罕默德 · 阿米乃易卜拉欣