2009-01-29 45 views
18

今天我不得不修復一些使用線程的較老的VB.NET 1.0代碼。問題是從工作線程而不是UI線程更新UI元素。我花了一些時間才發現可以使用InvokeRequired的斷言來查找問題。如何在.NET中編寫安全/正確的多線程代碼?

除了上面提到的併發修改問題之外,還有可能遇到的死鎖,競態條件等。 由於調試/修復線程問題是一種痛苦,我想知道如何才能減少這方面的編碼錯誤/錯誤,以及如何更容易地找到它們中的任何一個。那麼,我要問的是:

  • 有什麼好看的圖案編寫多線程代碼時要遵循?什麼是Dos和不該做的事?
  • 你用什麼技術來調試線程問題?

請在適用和可能的情況下提供一些示例代碼。答案應該與.NET框架(任何版本)相關。

回答

24

這可能是一個大規模的列表 - 閱讀Joe Duffy的優秀「Concurrent Programming On Windows」瞭解更多細節。這是一個很值得傾吐心事......

  • 儘量避免調入的代碼顯著塊,而你自己的鎖
  • 避免鎖死在引用該代碼的類外還可以鎖定
  • 如果您需要一次獲得多個鎖,總是以相同的順序獲取這些鎖
  • 在合理的情況下,使用不可變類型 - 它們可以在線程之間自由共享
  • 除了不可變類型之外,嘗試避免需要在線程之間共享數據
  • 避免嘗試使您的類型線程安全;大多數類型不必是,通常這需要共享數據的代碼將需要控制鎖定本身
  • 在WinForms應用程序:
    • 不要執行任何長時間運行或阻塞操作UI線程
    • 請勿觸摸UI線程以外的任何線程的UI。 (使用BackgroundWorker,Control.Invoke/BeginInvoke)
  • 儘可能避免線程局部變量(又名線程靜態) - 它們可能會導致意外的行爲,特別是在ASP.NET中,請求可能由不同服務線程(搜索「線程敏捷性」和ASP.NET)
  • 不要試圖變得聰明。無鎖併發碼巨大很難找到正確的。
  • 記錄您的類型的線程模型(和線程安全性)
  • Monitor.Wait應該幾乎總是與某種檢查結合使用,在while循環中(即while(I can not proceed)Monitor)。等待(監視器))
  • 每次使用Monitor.Pulse和Monitor.PulseAll時都要仔細考慮一下。
  • 插入Thread.Sleep使問題消失永遠不是一個真正的解決方案。
  • 查看「並行擴展」和「協調和併發運行時」是使併發更簡單的方法。並行擴展將成爲.NET 4.0的一部分。

在調試方面,我沒有太多的建議。使用Thread.Sleep來增加看到競爭條件和僵局的機會是可行的,但在你知道該把它放到哪裏之前,你必須對錯誤有一個合理的理解。記錄非常方便,但不要忘記代碼會進入某種量子狀態 - 通過記錄觀察它幾乎肯定會改變它的行爲!

+3

「但不忘記代碼進入某種量子狀態「 - *嘆*我很清楚這個問題。 – 2009-01-29 21:02:27

11

我不知道有多好,這將有助於你正在使用的特定應用程序,但這裏有兩種方法從函數式編程借用編寫多線程代碼:

不可變對象

如果你需要在線程之間共享狀態,狀態應該是不可變的。如果一個線程需要對該對象進行更改,它會隨着更改而創建對象的全新版本,而不是改變對象的狀態。

不變性本身並不限制你可以編寫的代碼的類型,也不是低效的。有很多不可變堆棧的實現,形成映射和集合的基礎的各種不可變樹,以及其他類型的不可變數據結構,許多(如果不是全部的話)不可變數據結構與其可變對象一樣有效。

因爲對象是不可變的,所以它不可能讓一個線程在你的鼻子下改變共享狀態。這意味着你不需要獲取鎖來編寫多線程代碼。這種方法消除了與死鎖,活鎖和競賽有關的一整類錯誤。

二郎式消息傳遞

你不需要學習語言,但看看二郎,看看它如何處理併發。 Erlang應用程序可以無限期地擴展,因爲每個進程完全獨立於其他所有進程(注意:這些並不完全是進程,但也不完全是線程)。

進程啓動並簡單地旋轉一個等待消息的循環:消息以元組的形式接收,然後進程可以進行模式匹配以查看消息是否有意義。進程可以發送其他消息,但他們對收到消息的人無動於衷。

這種風格的優勢在於消除了鎖,當一個進程失敗時,它不會降低您的整個應用程序。這裏的二郎式併發的一個很好的總結:http://www.defmacro.org/ramblings/concurrency.html

+1

很好的答案。很高興看到一些關注高級方法的問題,而不是僅列出所有.NET特定的同步原語。 :) – jalf 2009-01-31 22:12:55

1

這些步驟編寫質量(更易於閱讀和理解)多線程代碼:

  1. 退房的Power Threading Library通過傑弗裏裏希特
  2. 觀看視頻 - 驚訝
  3. 花一些時間來深入瞭解真正發生的事情,閱讀「併發事件」的文章找到here
  4. 開始編寫健壯,安全的多線程應用程序!
  5. 實現它仍然不是那麼簡單,犯一些錯誤,並從中學習... ...重複重複...重複:-)
2

使用的FIFO。其中很多。這是硬件程序員的古老祕密,它不止一次地保存了我的培根。

1

似乎沒人回答如何調試多線程程序的問題。這是一個真正的挑戰,因爲如果存在錯誤,就需要實時進行調查,而使用Visual Studio等大多數工具幾乎不可能。唯一的解決辦法是寫的痕跡,但跟蹤本身應該:

  1. 沒有增加任何延遲
  2. 沒有使用任何鎖定
  3. 進行多線程安全
  4. 跟蹤以正確的順序發生了什麼事。

這聽起來像一個不可能完成的任務,但可以通過將跟蹤寫入內存來輕鬆實現。在C#中,它會是這個樣子:

public const int MaxMessages = 0x100; 
string[] messages = new string[MaxMessages]; 
int messagesIndex = -1; 

public void Trace(string message) { 
    int thisIndex = Interlocked.Increment(ref messagesIndex); 
    messages[thisIndex] = message; 
} 

的方法跟蹤()是多線程安全的,無阻塞,可以從任何線程調用。在我的電腦上,執行需要大約2微秒,這應該足夠快。

在您認爲出現問題的地方添加Trace()指令,讓程序運行,等到錯誤發生時停止跟蹤,然後調查跟蹤以查找任何錯誤。

用於這種方法也收集線程和定時信息更詳細的說明,回收緩衝輸出跟蹤很好,你可以找到在: CodeProject上:實時調試多線程代碼1