C# 线程手册 第三章 使用线程
概要
在之前章節,我們已經討論過線程在開發多用戶應用程序時扮演的重要角色。我們已經使用線程來解決一些重要的問題,比如讓多個用戶或者客戶端在同一時間訪問同一個資源。然而,在學習過程中我們忽略了一個問題,現在到了處理這個問題的時候了:如果一個用戶改變了資源的狀態,同時另外一個用戶也想改變同一個資源的狀態的話,會發生什么?
舉個例子,假設有一臺ATM, 丹尼爾和他夫人決定通過ATM從他們的共同支票賬戶中取出1000美元。不幸的是他們忘了誰要做這件事情。不湊巧的是他們倆同時從兩臺不同的ATM 機器訪問同一個賬戶,如果運行在銀行后臺的程序不是線程安全的話,那么很有可能每臺ATM機在檢測賬戶時都認為余額足夠并因此分別向夫妻倆各支付1000美元。丹尼爾夫妻倆導致銀行后臺程序在同一時間生成兩個訪問賬戶數據庫的線程。在一個理想情況下,當一個用戶在更新他們的賬戶時,任何其他人都不可以訪問同一個賬戶。簡而言之,當一個用戶訪問數據庫賬戶來做一些賬戶信息更新操作時,系統必須將響應賬戶鎖住。
.NET Framework 提供了一套特殊架構來處理類似問題。在任意時間允許有且僅有一個線程訪問一個資源的技術稱作同步。同步是一種程序員用來對重要資源進行線程安全訪問的技術。
為何要擔心同步問題?
.NET 開發人員在設計一個多線程應用程序時需要仔細考慮同步問題主要是基于以下兩個原因:
? 1. 競爭條件
? 2. 保證線程安全
由于.NET Framework 內部支持線程, 所以可能你開發的所有類都最終要用在多線程應用程序中。你不需要(也不應該)把每個類都設計成線程安全,因為線程安全不是免費的。但是你至少應該在每次設計一個.NET類時考慮一下線程安全,考慮一下你的類會不會同時被多個線程訪問。線程安全的開銷以及如何讓一個類成為線程安全的會在本章的后續部分討論。不要擔心多線程訪問本地變量,方法參數以及返回值,因為這些變量存儲在堆棧中,而堆棧本身就是線程安全的。實例和類變量不是存儲在堆棧中,所以除非你為自己的類設計了線程安全,否則它們不是線程安全的。
在我們對線程同步作深入介紹之前,我們再了解一下本章開始時提到的ATM的例子。圖1 描繪了丹尼爾夫婦在同一時間從同一個賬戶中取出1000美元。當一個線程訪問一個資源并使其狀態非法,同時另外一個線程使用這個狀態非法的對象而導致不可預知的結果,這種行為稱作競爭條件。為了避免競爭條件,我們需要讓Withdraw()方法是線程安全的,以便于在任意時間有且僅有一個線程訪問這個方法。
圖1
?
至少有三種方法可以使一個對象是線程安全的:
? 1. 在代碼中對關鍵部分進行同步
? 2. 讓對象狀態不變,或者說讓對象稱為常量
? 3. 使用一個線程安全的包裝
對關鍵部分進行同步
為了避免多個線程在同一時間更新同一個資源而導致的不可預知的影響,我們需要嚴格限制對資源的訪問,比如在任意時間只允許一個線程更新資源,換句話說,讓資源成為線程安全的。讓一個對象或者一個實例變量成為線程安全的最直觀方式是確定其關鍵部分并對其關鍵部分進行同步。程序中的關鍵部分是指可能在同一時間被多個線程調用來更新某個對象狀態的代碼。例如,在上面提到過的場景中丹尼爾夫婦想要在同一時間訪問Withdraw()方法,則Withdraw()方法就是關鍵部分且需要是線程安全的。實現這個方案的最簡單方式就是對Withdraw()方法進行同步以便于在任意時間僅有一個線程可以訪問它。在執行過程中不可以被中斷的過程稱作原子。原子是一個不可再分的單位,原子操作是以一個完整的單元執行的代碼部分-看起來像一條單一的處理器指令。通過讓Withdraw()方法變成原子的,我們可以確定在一個線程對賬戶狀態修改完之前其他線程不可能改變同一個賬戶狀態。下面是以偽碼表示的非線程安全Account類:
class NonThreadSafeAccount { public ApprovedOrNot Withdraw(Amount) { //1. Make sure that the user has enough cash(Check the Balance) //2. Update the Account with the new balance //3. Send approval to the ATM } }下面是由偽碼表示的線程安全的Account類:
class NonThreadSafeAccount {public ApprovedOrNot Withdraw(Amount){lock this section(access for only one thread){//1. Check the Account Balance//2. Update the Account with the new balance//3. Send approval to the ATM}} }在第一個代碼片段中,可能有多于兩個線程在同一時間進入關鍵部分,所以有可能在同一時間有兩個線程檢查賬戶余額,而兩個線程都會得到賬戶余額1000美元。由于這個原因,兩個用戶可能都從ATM機取出1000美元,這導致賬戶異常性地透支。
在第二個代碼片段中,在任意時間只允許有一個線程訪問關鍵部分。假設丹尼爾首先獲得時間片,那么他就會在索菲亞之前進入Withdraw()方法。所以當丹尼爾的線程開始執行Withdraw()方法時,索菲亞的線程不被允許進入關鍵部分且必須等待丹尼爾的線程離開關鍵部分。丹尼爾的線程檢查賬戶余額,然后把賬戶余額更新為0,并向ATM機返回允許的指令以指示ATM 出鈔。鈔票取走之前,其他線程不可以訪問丹尼爾夫婦的共同賬戶。當丹尼爾取到錢以后,他的妻子進入到Withdraw()方法。現在,當這個方法檢查賬戶余額時,返回值是0美元,接下來就會告訴ATM機賬戶余額不足不能取錢。
讓賬戶對象成為常量
另外一種可以讓一個對象線程安全的方式是讓對象狀態不可變。一個狀態不可變的對象是指那種一經創建狀態就不可以更改的對象。可以通過當賬戶對象創建以后不允許任何線程來修改其狀態。采用這種方案,我們把變量只讀操作和變量可寫操作分開。讀取實例變量的關鍵部分不變,而對改變實例變量的關鍵部分進行修改。比如,不返回當前對象的狀態,而是通過創建一個當前對象的副本并將其引用返回。在這個方法中,我們不需要鎖住關鍵部分,因為事實上沒有方法修改對象實例變量,所以一個不可變的對象是線程安全的。
使用一個線程安全的包裝
第三種讓一個對象線程安全的方案是為對象寫一個包裝類,并讓包裝類成為線程安全而非讓對象本身線程安全。對象本身不變,新的包裝類包含線程安全代碼的同步部分。下面代碼片段是Account對象的一個包裝類:
class AccountWrapper {private Account a;public AccountWrapper(Account a){this.a = a;}public bool Withdraw(double amount){lock(a){return this.a.Withdraw(amount);}} }AccountWrapper 類是Account類的一個線程安全包裝。AccountWrapper類有一個私有的Account實例變量以便于沒有其他對象或者線程可以訪問Account變量。在這個方法中,Account對象沒有任何線程安全特性,而所有的線程安全都由AccountWrapper類提供。
當你使用一些第三方類庫中的非線程安全的類時這個方法非常有用。例如,假設銀行已經有了一個在大型機系統上開發軟件的Account類,為了一致性,現在銀行想使用同樣的Account類來實現ATM軟件。從銀行提供給我們的關于Account 類的文檔中,它清楚地說明Account類是非線程安全的。同時由于安全原因我們不能訪問Account源代碼。在這種情況下,我們不得不采用線程安全包裝方法來開發一個線程安全的AccountWrapper類作為Account類的擴展。包裝類用來向非線程安全資源中添加同步。所有的同步邏輯都在包裝類中且不影響非線程安全類的完整性。
?
下一篇將介紹.NET 中對同步的支持…
總結
以上是生活随笔為你收集整理的C# 线程手册 第三章 使用线程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 支付宝转帐需要手续费吗
- 下一篇: Tegra3 vSMP架构Android