線程(一)php
文章系參考轉載,英文原文網址請參考:http://www.albahari.com/threading/html
做者 Joseph Albahari, 翻譯 Swanky Wu程序員
中文翻譯做者把原文放在了"google 協做"上面,GFW屏蔽,不能訪問和查看,所以我根據譯文和英文原版整理轉載到園子裏面。web
本系列文章能夠算是一本很出色的C#線程手冊,思路清晰,要點都有介紹,看了後對C#的線程及同步等有了更深刻的理解。數據庫
C#支持經過多線程並行地執行代碼,一個線程有它獨立的執行路徑,可以與其它的線程同時地運行。一個C#程序開始於一個單線程,這個單線程是被CLR和操做系統(也稱爲「主線程」)自動建立的,並具備多線程建立額外的線程。這裏的一個簡單的例子及其輸出:安全
除非被指定,不然全部的例子都假定如下命名空間被引用了:
using System;
using System.Threading;服務器
1
2
3
4
5
6
7
8
9
10
11
|
class
ThreadTest {
static
void
Main() {
Thread t =
new
Thread (WriteY);
t.Start();
// Run WriteY on the new thread
while
(
true
) Console.Write (
"x"
);
// Write 'x' forever
}
static
void
WriteY() {
while
(
true
) Console.Write (
"y"
);
// Write 'y' forever
}
}
|
主線程建立了一個新線程「t」,它運行了一個重複打印字母"y"的方法,同時主線程重複但因字母「x」。CLR分配每一個線程到它本身的內存堆棧上,來保證局部變量的分離運行。在接下來的方法中咱們定義了一個局部變量,而後在主線程和新建立的線程上同時地調用這個方法。cookie
1
2
3
4
5
6
7
8
9
|
static
void
Main() {
new
Thread (Go).Start();
// Call Go() on a new thread
Go();
// Call Go() on the main thread
}
static
void
Go() {
// Declare and use a local variable - 'cycles'
for
(
int
cycles = 0; cycles < 5; cycles++) Console.Write (
'?'
);
}
|
變量cycles的副本分別在各自的內存堆棧中建立,輸出也同樣,可預見,會有10個問號輸出。當線程們引用了一些公用的目標實例的時候,他們會共享數據。下面是實例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
ThreadTest {
bool
done;
static
void
Main() {
ThreadTest tt =
new
ThreadTest();
// Create a common instance
new
Thread (tt.Go).Start();
tt.Go();
}
// Note that Go is now an instance method
void
Go() {
if
(!done) { done =
true
; Console.WriteLine (
"Done"
); }
}
}
由於在相同的<b>ThreadTest</b>實例中,兩個線程都調用了<b>Go()</b>,它們共享了<b>done</b>字段,這個結果輸出的是一個
"Done"
,而不是兩個。
|
1
|
<a href=
"http://images.cnblogs.com/cnblogs_com/miniwiki/WindowsLiveWriter/C_12936/image_6.png"
><img height=
"45"
width=
"640"
src=
"http://images.cnblogs.com/cnblogs_com/miniwiki/WindowsLiveWriter/C_12936/image_thumb_2.png"
align=
"left"
alt=
"image"
border=
"0"
title=
"image"
style=
"display: inline; margin-left: 0px; margin-right: 0px; border-width: 0px;"
></a>
|
靜態字段提供了另外一種在線程間共享數據的方式,下面是一個以done爲靜態字段的例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ThreadTest {
static
bool
done;
// Static fields are shared between all threads
static
void
Main() {
new
Thread (Go).Start();
Go();
}
static
void
Go() {
if
(!done) { done =
true
; Console.WriteLine (
"Done"
); }
}
}
|
上述兩個例子足以說明, 另外一個關鍵概念, 那就是線程安全(或反之,它的不足之處! ) 輸出其實是不肯定的:它可能(雖然不大可能) , "Done" ,能夠被打印兩次。然而,若是咱們在Go方法裏調換指令的順序, "Done"被打印兩次的機會會大幅地上升:
1
2
3
|
static
void
Go() {
if
(!done) { Console.WriteLine (
"Done"
); done =
true
; }
}
|
問題就是一個線程在判斷if塊的時候,正好另外一個線程正在執行WriteLine語句——在它將done設置爲true以前。
補救措施是當讀寫公共字段的時候,提供一個排他鎖;C#提供了lock語句來達到這個目的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
ThreadSafe {
static
bool
done;
static
object
locker =
new
object
();
static
void
Main() {
new
Thread (Go).Start();
Go();
}
static
void
Go() {
lock
(locker) {
if
(!done) { Console.WriteLine (
"Done"
); done =
true
; }
}
}
}
|
當兩個線程爭奪一個鎖的時候(在這個例子裏是locker),一個線程等待,或者說被阻止到那個鎖變的可用。在這種狀況下,就確保了在同一時刻只有一個線程能進入臨界區,因此"Done"只被打印了1次。代碼以如此方式在不肯定的多線程環境中被叫作線程安全。
臨時暫停,或阻止是多線程的協同工做,同步活動的本質特徵。等待一個排它鎖被釋放是一個線程被阻止的緣由,另外一個緣由是線程想要暫停或Sleep一段時間:
1
|
Thread.Sleep (TimeSpan.FromSeconds (30));
// Block for 30 seconds
|
一個線程也能夠使用它的Join方法來等待另外一個線程結束:
1
2
3
|
Thread t =
new
Thread (Go);
// Assume Go is some static method
t.Start();
t.Join();
// Wait (block) until thread t ends
|
一個線程,一旦被阻止,它就再也不消耗CPU的資源了。
線程是如何工做的
線程被一個線程協調程序管理着——一個CLR委託給操做系統的函數。線程協調程序確保將全部活動的線程被分配適當的執行時間;而且那些等待或阻止的線程——好比說在排它鎖中、或在用戶輸入——都是不消耗CPU時間的。
在單核處理器的電腦中,線程協調程序完成一個時間片以後迅速地在活動的線程之間進行切換執行。這就致使「波濤洶涌」的行爲,例如在第一個例子,每次重複的X 或 Y 塊至關於分給線程的時間片。在Windows XP中時間片一般在10毫秒內選擇要比CPU開銷在處理線程切換的時候的消耗大的多。(即一般在幾微秒區間)
在多核的電腦中,多線程被實現成混合時間片和真實的併發——不一樣的線程在不一樣的CPU上運行。這幾乎能夠確定仍然會出現一些時間切片, 因爲操做系統的須要服務本身的線程,以及一些其餘的應用程序。
線程因爲外部因素(好比時間片)被中斷被稱爲被搶佔,在大多數狀況下,一個線程方面在被搶佔的那一時那一刻就失去了對它的控制權。
線程 vs. 進程
屬於一個單一的應用程序的全部的線程邏輯上被包含在一個進程中,進程指一個應用程序所運行的操做系統單元。
線程於進程有某些類似的地方:好比說進程一般以時間片方式與其它在電腦中運行的進程的方式與一個C#程序線程運行的方式大體相同。兩者的關鍵區別在於進程彼此是徹底隔絕的。線程與運行在相同程序其它線程共享(堆heap)內存,這就是線程爲什麼如此有用:一個線程能夠在後臺讀取數據,而另外一個線程能夠在前臺展示已讀取的數據。
什麼時候使用多線程
多線程程序通常被用來在後臺執行耗時的任務。主線程保持運行,而且工做線程作它的後臺工做。對於Windows Forms程序來講,若是主線程試圖執行冗長的操做,鍵盤和鼠標的操做會變的遲鈍,程序也會失去響應。因爲這個緣由,應該在工做線程中運行一個耗時任務時添加一個工做線程,即便在主線程上有一個有好的提示「處理中...」,以防止工做沒法繼續。這就避免了程序出現由操做系統提示的「沒有相應」,來誘使用戶強制結束程序的進程而致使錯誤。模式對話框還容許實現「取消」功能,容許繼續接收事件,而實際的任務已被工做線程完成。BackgroundWorker剛好能夠輔助完成這一功能。
在沒有用戶界面的程序裏,好比說Windows Service, 多線程在當一個任務有潛在的耗時,由於它在等待另臺電腦的響應(好比一個應用服務器,數據庫服務器,或者一個客戶端)的實現特別有意義。用工做線程完成任務意味着主線程能夠當即作其它的事情。
另外一個多線程的用途是在方法中完成一個複雜的計算工做。這個方法會在多核的電腦上運行的更快,若是工做量被多個線程分開的話(使用Environment.ProcessorCount屬性來偵測處理芯片的數量)。
一個C#程序稱爲多線程的能夠經過2種方式:明確地建立和運行多線程,或者使用.NET framework的暗中使用了多線程的特性——好比BackgroundWorker類, 線程池,threading timer,遠程服務器,或Web Services或ASP.NET程序。在後面的狀況,人們別無選擇,必須使用多線程;一個單線程的ASP.NET web server不是太酷,即便有這樣的事情;幸運的是,應用服務器中多線程是至關廣泛的;惟一值得關心的是提供適當鎖機制的靜態變量問題。
什麼時候不要使用多線程
多線程也一樣會帶來缺點,最大的問題是它使程序變的過於複雜,擁有多線程自己並不複雜,複雜是的線程的交互做用,這帶來了不管是否交互是不是有意的,都會帶來較長的開發週期,以及帶來間歇性和非重複性的bugs。所以,要麼多線程的交互設計簡單一些,要麼就根本不使用多線程。除非你有強烈的重寫和調試慾望。
當用戶頻繁地分配和切換線程時,多線程會帶來增長資源和CPU的開銷。在某些狀況下,太多的I/O操做是很是棘手的,當只有一個或兩個工做線程要比有衆多的線程在相同時間執行任務塊的多。稍後咱們將實現生產者/耗費者 隊列,它提供了上述功能。
線程用Thread類來建立, 經過ThreadStart委託來指明方法從哪裏開始運行,下面是ThreadStart委託如何定義的:
1
|
public
delegate
void
ThreadStart();
|
調用Start方法後,線程開始運行,線程一直到它所調用的方法返回後結束。下面是一個例子,使用了C#的語法建立TheadStart委託:
1
2
3
4
5
6
7
|
class
ThreadTest {
static
void
Main() {
Thread t =
new
Thread (
new
ThreadStart (Go));
t.Start();
// Run Go() on the new thread.
Go();
// Simultaneously run Go() in the main thread.
}
static
void
Go() { Console.WriteLine (
"hello!"
); }
|
在這個例子中,線程t執行Go()方法,大約與此同時主線程也調用了Go(),結果是兩個幾乎同時hello被打印出來:
一個線程能夠經過C#堆委託簡短的語法更便利地建立出來:
1
2
3
4
5
6
7
|
static
void
Main() {
Thread t =
new
Thread (Go);
// No need to explicitly use ThreadStart
t.Start();
...
}
static
void
Go() { ... }
在這種狀況,ThreadStart被編譯器自動推斷出來,另外一個快捷的方式是使用匿名方法來啓動線程:
|
1
2
3
4
|
static
void
Main() {
Thread t =
new
Thread (
delegate
() { Console.WriteLine (
"Hello!"
); });
t.Start();
}
|
線程有一個IsAlive屬性,在調用Start()以後直到線程結束以前一直爲true。一個線程一旦結束便不能從新開始了。
將數據傳入ThreadStart中
話又說回來,在上面的例子裏,咱們想更好地區分開每一個線程的輸出結果,讓其中一個線程輸出大寫字母。咱們傳入一個狀態字到Go中來完成整個任務,但咱們不能使用ThreadStart委託,由於它不接受參數,所幸的是,.NET framework定義了另外一個版本的委託叫作ParameterizedThreadStart, 它能夠接收一個單獨的object類型參數:
1
2
|
public
delegate
void
ParameterizedThreadStart (
object
obj);
以前的例子看起來是這樣的:
|
1
|
|
1
2
3
4
5
6
7
8
9
10
|
class
ThreadTest {
static
void
Main() {
Thread t =
new
Thread (Go);
t.Start (
true
);
// == Go (true)
Go (
false
);
}
static
void
Go (
object
upperCase) {
bool
upper = (
bool
) upperCase;
Console.WriteLine (upper ?
"HELLO!"
:
"hello!"
);
}
|
在整個例子中,編譯器自動推斷出ParameterizedThreadStart委託,由於Go方法接收一個單獨的object參數,就像這樣寫:
1
2
|
Thread t =
new
Thread (
new
ParameterizedThreadStart (Go));
t.Start (
true
);
|
ParameterizedThreadStart的特性是在使用以前咱們必需對咱們想要的類型(這裏是bool)進行裝箱操做,而且它只能接收一個參數。
一個替代方案是使用一個匿名方法調用一個普通的方法以下:
1
2
3
4
5
|
static
void
Main() {
Thread t =
new
Thread (
delegate
() { WriteText (
"Hello"
); });
t.Start();
}
static
void
WriteText (
string
text) { Console.WriteLine (text); }
|
優勢是目標方法(這裏是WriteText),能夠接收任意數量的參數,而且沒有裝箱操做。不過這須要將一個外部變量放入到匿名方法中,向下面的同樣:
1
2
3
4
5
6
7
|
static
void
Main() {
string
text =
"Before"
;
Thread t =
new
Thread (
delegate
() { WriteText (text); });
text =
"After"
;
t.Start();
}
static
void
WriteText (
string
text) { Console.WriteLine (text); }
|
匿名方法打開了一種怪異的現象,當外部變量被後來的部分修改了值的時候,可能會透過外部變量進行無心的互動。有意的互動(一般經過字段)被認爲是足夠了!一旦線程開始運行了,外部變量最好被處理成只讀的——除非有人願意使用適當的鎖。
另外一種較常見的方式是將對象實例的方法而不是靜態方法傳入到線程中,對象實例的屬性能夠告訴線程要作什麼,以下列重寫了原來的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class
ThreadTest {
bool
upper;
static
void
Main() {
ThreadTest instance1 =
new
ThreadTest();
instance1.upper =
true
;
Thread t =
new
Thread (instance1.Go);
t.Start();
ThreadTest instance2 =
new
ThreadTest();
instance2.Go();
// 主線程——運行 upper=false
}
void
Go() { Console.WriteLine (upper ?
"HELLO!"
:
"hello!"
); }
|
命名線程
線程能夠經過它的Name屬性進行命名,這非產有利於調試:能夠用Console.WriteLine打印出線程的名字,Microsoft Visual Studio能夠將線程的名字顯示在調試工具欄的位置上。線程的名字能夠在被任什麼時候間設置——但只能設置一次,重命名會引起異常。
程序的主線程也能夠被命名,下面例子裏主線程經過CurrentThread命名:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ThreadNaming {
static
void
Main() {
Thread.CurrentThread.Name =
"main"
;
Thread worker =
new
Thread (Go);
worker.Name =
"worker"
;
worker.Start();
Go();
}
static
void
Go() {
Console.WriteLine (
"Hello from "
+ Thread.CurrentThread.Name);
}
}
|
前臺和後臺線程
線程默認爲前臺線程,這意味着任何前臺線程在運行都會保持程序存活。C#也支持後臺線程,當全部前臺線程結束後,它們不維持程序的存活。
改變線程從前臺到後臺不會以任何方式改變它在CPU協調程序中的優先級和狀態。
線程的IsBackground屬性控制它的先後臺狀態,以下實例:
1
2
3
4
5
6
7
|
class
PriorityTest {
static
void
Main (
string
[] args) {
Thread worker =
new
Thread (
delegate
() { Console.ReadLine(); });
if
(args.Length > 0) worker.IsBackground =
true
;
worker.Start();
}
}
|
若是程序被調用的時候沒有任何參數,工做線程爲前臺線程,而且將等待ReadLine語句來等待用戶的觸發回車,這期間,主線程退出,可是程序保持運行,由於一個前臺線程仍然活着。
另外一方面若是有參數傳入Main(),工做線程被賦值爲後臺線程,當主線程結束程序馬上退出,終止了ReadLine。
後臺線程終止的這種方式,使任何最後操做都被規避了,這種方式是不太合適的。好的方式是明確等待任何後臺工做線程完成後再結束程序,可能用一個timeout(大多用Thread.Join)。若是由於某種緣由某個工做線程沒法完成,能夠用試圖終止它的方式,若是失敗了,再拋棄線程,容許它與 與進程一塊兒消亡。(記錄是一個難題,但這個場景下是有意義的)
擁有一個後臺工做線程是有益的,最直接的理由是它當提到結束程序它老是可能有最後的發言權。交織以不會消亡的前臺線程,保證程序的正常退出。拋棄一個前臺工做線程是尤其險惡的,尤爲對Windows Forms程序,由於程序直到主線程結束時才退出(至少對用戶來講),可是它的進程仍然運行着。在Windows任務管理器它將從應用程序欄消失不見,但卻能夠在進程欄找到它。除非用戶找到並結束它,它將繼續消耗資源,並可能阻止一個新的實例的運行從開始或影響它的特性。
對於程序失敗退出的廣泛緣由就是存在「被忘記」的前臺線程。
線程優先級
線程的Priority 屬性肯定了線程相對於其它同一進程的活動的線程擁有多少執行時間,如下是級別:
1
|
enum
ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
|
只有多個線程同時爲活動時,優先級纔有做用。
設置一個線程的優先級爲高一些,並不意味着它能執行實時的工做,由於它受限於程序的進程的級別。要執行實時的工做,必須提高在System.Diagnostics 命名空間下Process的級別,像下面這樣:(我沒有告訴你如何作到這一點:))
1
|
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
|
ProcessPriorityClass.High 實際上是一個短暫缺口的過程當中的最高優先級別:Realtime。設置進程級別到Realtime通知操做系統:你不想讓你的進程被搶佔了。若是你的程序進入一個偶然的死循環,能夠預期,操做系統被鎖住了,除了關機沒有什麼能夠拯救你了!基於此,High大致上被認爲最高的有用進程級別。
若是一個實時的程序有一個用戶界面,提高進程的級別是不太好的,由於當用戶界面UI過於複雜的時候,界面的更新耗費過多的CPU時間,拖慢了整臺電腦。(雖然在寫這篇文章的時候,在互聯網電話程序Skype僥倖地這麼作, 也許是由於它的界面至關簡單吧。) 下降主線程的級別、提高進程的級別、確保實時線程不進行界面刷新,但這樣並不能避免電腦愈來愈慢,由於操做系統仍會撥出過多的CPU給整個進程。最理想的方案是使實時工做和用戶界面在不一樣的進程(擁有不一樣的優先級)運行,經過Remoting或共享內存方式進行通訊,共享內存須要Win32 API中的 P/Invoking。(能夠搜索看看CreateFileMapping 和 MapViewOfFile)
異常處理
任何線程建立範圍內try/catch/finally塊,當線程開始執行便再也不與其有任何關係。考慮下面的程序:
1
2
3
4
5
6
7
8
9
10
11
|
public
static
void
Main() {
try
{
new
Thread (Go).Start();
}
catch
(Exception ex) {
// 不會在這獲得異常
Console.WriteLine (
"Exception!"
);
}
static
void
Go() {
throw
null
; }
}
|
1
|
這裏
try
/
catch
語句一點用也沒有,新建立的線程將引起NullReferenceException異常。當你考慮到每一個線程有獨立的執行路徑的時候,便知道這行爲是有道理的,
|
1
|
補救方法是在線程處理的方法內加入他們本身的異常處理:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
static
void
Main() {
new
Thread (Go).Start();
}
static
void
Go() {
try
{
...
throw
null
;
// 這個異常在下面會被捕捉到
...
}
catch
(Exception ex) {
記錄異常日誌,而且或通知另外一個線程
咱們發生錯誤
...
}
|
從.NET 2.0開始,任何線程內的未處理的異常都將致使整個程序關閉,這意味着忽略異常再也不是一個選項了。所以爲了不由未處理異常引發的程序崩潰,try/catch塊須要出如今每一個線程進入的方法內,至少要在產品程序中應該如此。對於常用「全局」異常處理的Windows Forms程序員來講,這可能有點麻煩,像下面這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
using
System;
using
System.Threading;
using
System.Windows.Forms;
static
class
Program {
static
void
Main() {
Application.ThreadException += HandleError;
Application.Run (
new
MainForm());
}
static
void
HandleError (
object
sender, ThreadExceptionEventArgs e) {
記錄異常或者退出程序或者繼續運行...
}
}
|
Application.ThreadException事件在異常被拋出時觸發,以一個Windows信息(好比:鍵盤,鼠標活着 "paint" 等信息)的方式,簡言之,一個Windows Forms程序的幾乎全部代碼。雖然這看起來很完美,它令人產生一種虛假的安全感——全部的異常都被中央異常處理捕捉到了。由工做線程拋出的異常即是一個沒有被Application.ThreadException捕捉到的很好的例外。(在Main方法中的代碼,包括構造器的形式,在Windows信息開始前先執行)
.NET framework爲全局異常處理提供了一個更低級別的事件:AppDomain.UnhandledException,這個事件在任何類型的程序(有或沒有用戶界面)的任何線程有任何未處理的異常觸發。儘管它提供了好的不得已的異常處理解決機制,可是這不意味着這能保證程序不崩潰,也不意味着能取消.NET異常對話框。
在產品程序中,明確地使用異常處理在全部線程進入的方法中是必要的,能夠使用包裝類和幫助類來分解工做來完成任務,好比使用BackgroundWorker類(在第三部分進行討論)
下面的表格列展了.NET對協調或同步線程動做的可用的工具:
簡易阻止方法
構成 |
目的 |
Sleep |
阻止給定的時間週期 |
Join |
等待另外一個線程完成 |
鎖系統
構成 |
目的 |
跨進程? |
速度 |
lock |
確保只有一個線程訪問某個資源或某段代碼。 |
否 |
快 |
Mutex |
確保只有一個線程訪問某個資源或某段代碼。可被用於防止一個程序的多個實例同時運行。 |
是 |
中等 |
Semaphore |
確保不超過指定數目的線程訪問某個資源或某段代碼。 |
是 |
中等 |
(同步的狀況下也提夠自動鎖。)
信號系統
構成 |
目的 |
跨進程? |
速度 |
EventWaitHandle |
容許線程等待直到它受到了另外一個線程發出信號。 |
是 |
中等 |
Wait 和 Pulse* |
容許一個線程等待直到自定義阻止條件獲得知足。 |
否 |
中等 |
非阻止同步系統*
構成 |
目的 |
跨進程? |
速度 |
Interlocked* |
完成簡單的非阻止原子操做。 |
是(內存共享狀況下) |
很是快 |
volatile* |
容許安全的非阻止在鎖以外使用個別字段。 |
很是快 |
* 表明頁面將轉到第四部分
1.1 阻止 (Blocking)
當一個線程經過上面所列的方式處於等待或暫停的狀態,被稱爲被阻止。一旦被阻止,線程馬上放棄它被分配的
CPU時間,將它的ThreadState屬性添加爲WaitSleepJoin狀態,不在安排時間直到中止阻止。中止阻止在任意四種
狀況下發生(關掉電腦的電源可不算!):
當線程經過(不建議)Suspend 方法暫停,不認爲是被阻止了。
調用Thread.Sleep阻止當前的線程指定的時間(或者直到中斷):
1
2
3
4
5
6
|
static
void
Main() {
Thread.Sleep (0);
// 釋放CPU時間片
Thread.Sleep (1000);
// 休眠1000毫秒
Thread.Sleep (TimeSpan.FromHours (1));
// 休眠1小時
Thread.Sleep (Timeout.Infinite);
// 休眠直到中斷
}
|
更確切地說,Thread.Sleep放棄了佔用CPU,請求不在被分配時間直到給定的時間通過。Thread.Sleep(0)放棄
CPU的時間剛剛夠其它在時間片隊列裏的活動線程(若是有的話)被執行。
Thread.Sleep在阻止方法中是惟一的暫停汲取Windows Forms程序的Windows消息的方法,或COM環境中用於
單元模式。這在Windows Forms程序中是一個很大的問題,任何對主UI線程的阻止都將使程序失去相應。所以通常避
免這樣使用,不管信息汲取是否被「技術地」暫定與否。由COM遺留下來的宿主環境更爲複雜,在一些時候它決定中止,
而卻保持信息的汲取存活。微軟的 Chris Brumm 在他的博客中討論這個問題。(搜索: 'COM "Chris Brumme"')
線程類同時也提供了一個SpinWait方法,它使用輪詢CPU而非放棄CPU時間的方式,保持給定的迭代次數進行「無用
地繁忙」。50迭代可能等同於停頓大約一微秒,雖然這將取決於CPU的速度和負載。從技術上講,SpinWait並非一個阻
止的方法:一個處於spin-waiting的線程的ThreadState不是WaitSleepJoin狀態,而且也不會被其它的線程過早的中斷
(Interrupt)。SpinWait不多被使用,它的做用是等待一個在極短期(可能小於一微秒)內可準備好的可預期的資源,
而不用調用Sleep方法阻止線程而浪費CPU時間。不過,這種技術的優點只有在多處理器計算機:對單一處理器的電腦,
直到輪詢的線程結束了它的時間片以前,一個資源沒有機會改變狀態,這有違它的初衷。而且調用SpinWait常常會花費較
長的時間這自己就浪費了CPU時間。
線程能夠等待某個肯定的條件來明確輪詢使用一個輪詢的方式,好比:
1
|
while
(!proceed);
|
或者:
1
|
while
(DateTime.Now < nextStartTime);
|
這是很是浪費CPU時間的:對於CLR和操做系統而言,線程進行了一個重要的計算,因此分配了相應的資源!在這種狀態
下的輪詢線程不算是阻止,不像一個線程等待一個EventWaitHandle(通常使用這樣的信號任務來構建)。
阻止和輪詢組合使用能夠產生一些變換:
1
|
while
(!proceed) Thread.Sleep (x);
// "輪詢休眠!"
|
x越大,CPU效率越高,折中方案是增大潛伏時間,任何20ms的花費是微不足道的,除非循環中的條件是極其複雜的。
除了稍有延遲,這種輪詢和休眠的方式能夠結合的很是好。(但有併發問題,在第四部分討論)可能它最大的用處在於
程序員能夠放棄使用複雜的信號結構 來工做了。
你能夠經過Join方法阻止線程直到另外一個線程結束:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
JoinDemo {
static
void
Main() {
Thread t =
new
Thread (
delegate
() { Console.ReadLine();});
t.Start();
t.Join();
// 等待直到線程完成
Console.WriteLine (
"Thread t's ReadLine complete!"
);
}
}
|
Join方法也接收一個使用毫秒或用TimeSpan類的超時參數,當Join超時是返回false,若是線程已終止,則返回true 。
Join所帶的超時參數很是像Sleep方法,實際上下面兩行代碼幾乎差很少:
1
2
3
|
Thread.Sleep (1000);
Thread.CurrentThread.Join (1000);
|
(他們的區別明顯在於單線程的應用程序域與COM互操做性,源於先前描述Windows信息汲取部分:在阻止時,Join
保持信息汲取,Sleep暫停信息汲取。)
鎖實現互斥的訪問,被用於確保在同一時刻只有一個線程能夠進入特殊的代碼片斷,考慮下面的類:
1
2
3
4
5
6
7
8
|
class
ThreadUnsafe {
static
int
val1, val2;
static
void
Go() {
if
(val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
}
|
這不是線程安全的:若是Go方法被兩個線程同時調用,可能會獲得在某個線程中除數爲零的錯誤,由於val2可能被一個
線程設置爲零,而另外一個線程恰好執行到if和Console.WriteLine語句。
下面用lock來修正這個問題:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
ThreadSafe {
static
object
locker =
new
object
();
static
int
val1, val2;
static
void
Go() {
lock
(locker) {
if
(val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
}
}
|
在同一時刻只有一個線程能夠鎖定同步對象(在這裏是locker),任何競爭的的其它線程都將被阻止,直到這個鎖被釋放。若是有大於一個的線程競爭這個鎖,那麼他們將造成稱爲「就緒隊列」的隊列,以先到先得的方式受權鎖。互斥鎖有時被稱之對由鎖所保護的內容強迫串行化訪問,由於一個線程的訪問不能與另外一個重疊。在這個例子中,咱們保護了Go方法的邏輯,以及val1 和val2字段的邏輯。
一個等候競爭鎖的線程被阻止將在ThreadState上爲WaitSleepJoin狀態。稍後咱們將討論一個線程經過另外一個線程調用
Interrupt或Abort方法來強制地被釋放。這是一個至關高效率的技術能夠被用於結束工做線程。
C#的lock 語句其實是調用Monitor.Enter和Monitor.Exit,中間夾雜try-finally語句的簡略版,下面是實際發生在以前例
子中的Go方法:
1
2
3
4
5
6
7
8
|
Monitor.Enter (locker);
try
{
if
(val2 != 0) Console.WriteLine (val1 / val2);
val2 = 0;
}
finally
{ Monitor.Exit (locker);
}
|
在同一個對象上,在調用第一個以前Monitor.Enter而先調用了Monitor.Exit將引起異常。
Monitor 也提供了TryEnter方法來實現一個超時功能——也用毫秒或TimeSpan,若是得到了鎖返回true,反之沒有得到返回false,由於超時了。TryEnter也能夠沒有超時參數,「測試」一下鎖,若是鎖不能被獲取的話就馬上超時。
2.1 選擇同步對象
任何對全部有關係的線程均可見的對象均可以做爲同步對象,但要服從一個硬性規定:它必須是引用類型。也強烈建議同步對象最好私有在類裏面(好比一個私有實例字段)防止無心間從外部鎖定相同的對象。服從這些規則,同步對象能夠兼對象和保護兩種做用。好比下面List :
1
2
3
4
5
6
7
8
9
10
11
|
class
ThreadSafe {
List <
string
> list =
new
List <
string
>();
void
Test() {
lock
(list) {
list.Add (
"Item 1"
);
...
|
一個專門字段是經常使用的(如在先前的例子中的locker) , 由於它能夠精確控制鎖的範圍和粒度。用對象或類自己的類型做爲一個同步對象,即:
lock (this) { ... }
或:
lock (typeof (Widget)) { ... } // 保護訪問靜態
是很差的,由於這潛在的能夠在公共範圍訪問這些對象。
鎖並無以任何方式阻止對同步對象自己的訪問,換言之,x.ToString()不會因爲另外一個線程調用lock(x) 而被阻止,二者都要調用ock(x) 來完成阻止工做。
2.2 嵌套鎖定
線程能夠重複鎖定相同的對象,能夠經過屢次調用Monitor.Enter或lock語句來實現。當對應編號的Monitor.Exit被調用或最外面的lock語句完成後,對象那一刻被解鎖。這就容許最簡單的語法實現一個方法的鎖調用另外一個鎖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static
object
x =
new
object
();
static
void
Main() {
lock
(x) {
Console.WriteLine (
"I have the lock"
);
Nest();
Console.WriteLine (
"I still have the lock"
);
}
在這鎖被釋放
}
static
void
Nest() {
lock
(x) {
...
} 釋放了鎖?沒有徹底釋放!
}
|
線程只能在最開始的鎖或最外面的鎖時被阻止。
做爲一項基本規則,任何和多線程有關的會進行讀和寫的字段應當加鎖。甚至是極日常的事情——單一字段的賦值操做,都必須考慮到同步問題。在下面的例子中Increment和Assign 都不是線程安全的:
1
2
3
4
5
|
class
ThreadUnsafe {
static
int
x;
static
void
Increment() { x++; }
static
void
Assign() { x = 123; }
}
|
下面是Increment 和 Assign 線程安全的版本:
1
2
3
4
5
6
7
|
class
ThreadUnsafe {
static
object
locker =
new
object
();
static
int
x;
static
void
Increment() {
lock
(locker) x++; }
static
void
Assign() {
lock
(locker) x = 123; }
}
|
做爲鎖定另外一個選擇,在一些簡單的狀況下,你能夠使用非阻止同步,在第四部分討論(即便像這樣的語句須要同步的緣由)。
若是有不少變量在一些鎖中老是進行讀和寫的操做,那麼你能夠稱之爲原子操做。咱們假設x 和 y不停地讀和賦值,他們在鎖內經過
locker鎖定:
lock (locker) { if (x != 0) y /= x; }
你能夠認爲x 和 y 經過原子的方式訪問,由於代碼段沒有被其它的線程分開 或 搶佔,別的線程改變x 和 y是無效的輸出,你永遠不會獲得除數爲零的錯誤,保證了x 和 y老是被相同的排他鎖訪問。
2.5 性能考量
鎖定自己是很是快的,一個鎖在沒有堵塞的狀況下通常只需幾十納秒(十億分之一秒)。若是發生堵塞,任務切換帶來的開銷接近於數微秒(百萬分之一秒)的範圍內,儘管在線程重組實際的安排時間以前它可能花費數毫秒(千分之一秒)。而相反,與此相形見絀的是該使用鎖而沒使用的結果就是帶來數小時的時間,甚至超時。若是耗盡併發,鎖定會帶來副作用,死鎖和爭用鎖,耗盡併發因爲太多的代碼被放置到鎖語句中了,引發其它線程沒必要要的被阻止。死鎖是兩線程彼此等待被鎖定的內容,致使二者都沒法繼續下去。爭用鎖是兩個線程任一個均可以鎖定某個內容,若是「錯誤」的線程獲取了鎖,則致使程序錯誤。
對於太多的同步對象死鎖是很是容易出現的症狀,一個好的規則是開始於較少的鎖,在一個可信的狀況下涉及過多的阻止出現時,增長鎖的粒度。
線程安全的代碼是指在面對任何多線程狀況下,這代碼都沒有不肯定的因素。線程安全首先完成鎖,而後減小在線程間交互的可能性。
一個線程安全的方法,在任何狀況下能夠可重入式調用。通用類型在它們中不多是線程安全的,緣由以下:
所以線程安全常常只在須要實現的地方來實現,爲了處理一個特定的多線程狀況。
不過,有一些方法來「欺騙」,有龐大和複雜的類安全地運行在多線程環境中。一種是犧牲粒度包含大段的代碼——甚至在排他鎖中訪問全局對象,迫使在更高的級別上實現串行化訪問。這一策略也很關鍵,讓非線程安全的對象用於線程安全代碼中,避免了相同的互斥鎖被用於保護對在非線程安全對象的全部的屬性、方法和字段的訪問。
原始類型除外,不多的.NET framework類型實例相比於併發的只讀訪問,是線程安全的。責任在開放人員實現線程安全表明性地使用互斥鎖。
另外一個方式欺騙是經過最小化共享數據來最小化線程交互。這是一個很好的途徑,被暗中地用於「弱狀態」的中間層程序和web服務器。自多個客戶端請求同時到達,每一個請求來自它本身的線程(效力於ASP.NET,Web服務器或者遠程體系結構),這意味着它們調用的方法必定是線程安全的。弱狀態設計(因伸縮性好而流行)本質上限制了交互的能力,所以類不可以在每一個請求間持久保留數據。線程交互僅限於能夠被選擇建立的靜態字段,多半是在內存裏緩存經常使用數據和提供基礎設施服務,例如認證和審覈。
2.7線程安全與.NET Framework類型
鎖定可被用於將非線程安全的代碼轉換成線程安全的代碼。比較好的例子是在.NET framework方面,幾乎全部非基本類型的實例都不是線程安全的,而若是全部的訪問給定的對象都經過鎖進行了保護的話,他們能夠被用於多線程代碼中。看這個例子,兩個線程同時爲相同的List增長條目,而後枚舉它:.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class
ThreadSafe {
static
List <
string
> list =
new
List <
string
>();
static
void
Main() {
new
Thread (AddItems).Start();
new
Thread (AddItems).Start();
}
static
void
AddItems() {
for
(
int
i = 0; i < 100; i++)
lock
(list)list.Add (
"Item "
+ list.Count);
string
[] items;
lock
(list) items = list.ToArray();
foreach
(
string
s
in
items) Console.WriteLine (s);
}
}
|
在這種狀況下,咱們鎖定了list對象自己,這個簡單的方案是很好的。若是咱們有兩個相關的list,也許咱們就要鎖定一個共同的目標——單獨的一個字段,若是沒有其它的list出現,顯然鎖定它本身是明智的選擇。枚舉.NET的集合也不是線程安全的,在枚舉的時候另外一個線程改動list的話,會拋出異常。爲了避免直接鎖定枚舉過程,在這個例子中,咱們首先將項目複製到數組當中,這就避免了固定住鎖由於咱們在枚舉過程當中有潛在的耗時。
這裏的一個有趣的假設:想象若是List實際上爲線程安全的,如何解決呢?代碼會不多!舉例說明,咱們說咱們要增長一個項目到咱們假象的線程安全的list裏,以下:
if (!myList.Contains (newItem)) myList.Add (newItem);
不管與否list是否爲線程安全的,這個語句顯然不是!(所以,能夠說徹底線程安全的通用集合類是基本不存在的。.net4.0中,微軟提供了一組線程安全的並行集合類,可是都是特殊的通過處理過的,訪問方式都通過了限定。),上面的語句要實現線程安全,整個if語句必須放到一個鎖中,用來保護搶佔在判斷有無和增長新的之間。上述的鎖須要用於任何咱們須要修改list的地方,好比下面的語句須要被一樣的鎖包括住:
myList.Clear();
來保證它沒有搶佔以前的語句,換言之,咱們必須鎖定差很少全部非線程安全的集合類們。內置的線程安全,顯而易見是浪費時間!
在寫自定義組件的時候,你可能會反對這個觀點——爲何建造線程安全讓它容易的結果會變的多餘呢 ?
有一個爭論:在一個對象包上自定義的鎖僅在全部並行的線程知道、並使用這個鎖的時候才能工做,而若是鎖對象在更大的範圍內的時候,這個鎖對象可能不在這個鎖範圍內。最糟糕的狀況是靜態成員在公共類型中出現了,好比,想象靜態結構在DateTime上,DateTime.Now不是線程安全的,當有2個併發的調用可帶來錯亂的輸出或異常,補救方式是在其外進行鎖定,可能鎖定它的類型自己—— lock(typeof(DateTime))來圈住調用DateTime.Now,這會工做的,但只有全部的程序員贊成這樣作的時候。然而這並靠不住,鎖定一個類型被認爲是一件很是很差的事情。因爲這些理由,DateTime上的靜態成員是保證線程安全的,這是一個遍佈.NET framework一個廣泛模式——靜態成員是線程安全的,而一個實例成員則不是。從這個模式也能在寫自定義類型時獲得一些體會,不要建立一個不能線程安全的難題!
當寫公用組件的時候,好的習慣是不要忘記了線程安全,這意味着要單獨當心處理那些在其中或公共的靜態成員。
一個被阻止的線程能夠經過兩種方式被提早釋放:
這必須經過另外活動的線程實現,等待的線程是沒有能力對它的被阻止狀態作任何事情的。
在一個被阻止的線程上調用Interrupt 方法,將強迫釋放它,拋出ThreadInterruptedException異常,以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class
Program {
static
void
Main() {
Thread t =
new
Thread (
delegate
() {
try
{
Thread.Sleep (Timeout.Infinite);
}
catch
(ThreadInterruptedException) {
Console.Write (
"Forcibly "
);
}
Console.WriteLine (
"Woken!"
);
});
t.Start();
t.Interrupt();
}
}
Forcibly Woken!
|
中斷一個線程僅僅釋放它的當前的(或下一個)等待狀態:它並不結束這個線程(固然,除非未處理
ThreadInterruptedException異常)。
若是Interrupt被一個未阻止的線程調用,那麼線程將繼續執行直到下一次被阻止時,它拋出
ThreadInterruptedException異常。用下面的測試避免這個問題:
if ((worker.ThreadState & ThreadState.WaitSleepJoin) > 0)
worker.Interrupt();
這不是一個線程安全的方式,由於可能被搶佔了在if語句和worker.Interrupt間。
隨意中斷線程是危險的,由於任何框架或第三方方法在調用堆棧時可能會意外地在已訂閱的代碼上收到中斷。這一切將被認爲是線程被暫時阻止在一個鎖中或同步資源中,而且全部掛起的中斷將被踢開。若是這個方法沒有被設計成能夠被中斷(沒有適當處理finally塊)的對象可能剩下無用的狀態,或資源不徹底地被釋放。
中斷一個線程是安全的,當你知道它確切的在哪的時候。稍後咱們討論 信號系統,它提供這樣的一種方式。
被阻止的線程也能夠經過Abort方法被強制釋放,這與調用Interrupt類似,除了用ThreadAbortException異常代替了
ThreadInterruptedException異常,此外,異常將被從新拋出在catch裏(在試圖以有好方式處理異常的時候),直到Thread.ResetAbort在catch中被調用;在這期間線程的ThreadState爲AbortRequested。
在Interrupt 與 Abort 之間最大不一樣在於它們調用一個非阻止線程所發生的事情。Interrupt繼續工做直到下一次阻止發生,Abort在線程當前所執行的位置(可能甚至不在你的代碼中)拋出異常。終止一個非阻止的線程會帶來嚴重的後果,這在後面的 「終止線程」章節中將詳細討論。
圖1: 線程狀態關係圖
你能夠經過ThreadState屬性獲取線程的執行狀態。圖1將ThreadState列舉爲「層」。ThreadState被設計的很恐怖,它以按位計算的方式組合三種狀態「層」,每種狀態層的成員它們間都是互斥的,下面是全部的三種狀態「層」:
總的來講,ThreadState是按位組合零或每一個狀態層的成員!一個簡單的ThreadState例子:
Unstarted
Running
WaitSleepJoin
Background, Unstarted
SuspendRequested, Background, WaitSleepJoin
(所枚舉的成員有兩個歷來沒被用過,至少是當前CLR實現上:StopRequested 和 Aborted。)
還有更加複雜的,ThreadState.Running潛在的值爲0 ,所以下面的測試不工做:
if ((t.ThreadState & ThreadState.Running) > 0) ...
你必須用按位與非操做符來代替,或者使用線程的IsAlive屬性。可是IsAlive可能不是你想要的,它在被阻止或掛起的時候返回true(只有在線程未開始或已結束時它才爲true)。
假設你避開不推薦使用的Suspend 和 Resume方法,你能夠寫一個helper方法除去全部除了第一種狀態層的成員,容許簡單測試計算完成。線程的後臺狀態能夠經過IsBackground 獨立地得到,因此實際上只有第一種狀態層擁有有用的信息。
1
2
3
4
5
6
7
|
public
static
ThreadState SimpleThreadState (ThreadState ts)
{
return
ts & (ThreadState.Aborted | ThreadState.AbortRequested |
ThreadState.Stopped | ThreadState.Unstarted |
ThreadState.WaitSleepJoin);
}
|
ThreadState對調試或程序概要分析是無價之寶,與之不相稱的是多線程的協同工做,由於沒有一個機制存在:經過判斷ThreadState來執行信息,而不考慮ThreadState期間的變化。
lock語句(也稱爲Monitor.Enter / Monitor.Exit)是線程同步結構的一個例子。當lock對一段代碼或資源實施排他訪問時, 但有些同步任務是至關笨拙的或難以實現的,好比說須要傳輸信號給等待的工做線程使其開始任務執行。
Win32 API擁有豐富的同步系統,這在.NET framework以EventWaitHandle, Mutex 和 Semaphore類展露出來。而一些比有些更有用:例如Mutex類,在EventWaitHandle提供惟一的信號功能時,大多會成倍提升lock的效率。
這三個類都依賴於WaitHandle類,儘管從功能上講, 它們至關的不一樣。但它們作的事情都有一個共同點,那就是,被「點名」,這容許它們繞過操做系統進程工做,而不是隻能在當前進程裏繞過線程。
EventWaitHandle有兩個子類:AutoResetEvent 和 ManualResetEvent(不涉及到C#中的事件或委託)。這兩個類都派生自它們的基類:它們僅有的不一樣是它們用不一樣的參數調用基類的構造函數。性能方面,使用Wait Handles系統開銷會花費在微秒間,不會在它們使用的上下文中產生什麼後果。
AutoResetEvent在WaitHandle中是最有用的的類,它連同lock 語句是一個主要的同步結構。
AutoResetEvent就像一個用票經過的旋轉門:插入一張票,讓正確的人經過。類名字裏的「auto」實際上就是旋轉門自動關閉或「從新安排」後來的人讓其經過。一個線程等待或阻止經過在門上調用WaitOne方法(直到等到這個「one」,門纔開) ,票的插入則由調用Set方法。若是由許多線程調用WaitOne,在門前便造成了隊列,一張票可能來自任意某個線程——換言之,任何(非阻止)線程要經過AutoResetEvent對象調用Set方法來釋放一個被阻止的的線程。
也就是調用WaitOne方法的全部線程會阻塞到一個等待隊列,其餘非阻塞線程經過調用Set方法來釋放一個阻塞。而後AutoResetEvent繼續阻塞後面的線程。
若是Set調用時沒有任何線程處於等待狀態,那麼句柄保持打開直到某個線程調用了WaitOne 。這個行爲避免了在線程起身去旋轉門和線程插入票(哦,插入票是很是短的微秒間的事,真倒黴,你將必須不肯定地等下去了!)間的競爭。可是在沒人等的時候重複地在門上調用Set方法不會容許在一隊人都經過,在他們到達的時候:僅有下一我的能夠經過,多餘的票都被「浪費了"。
WaitOne 接受一個可選的超時參數——當等待以超時結束時這個方法將返回false,WaitOne在等待整段時間裏也通知離開當前的同步內容,爲了不過多的阻止發生。
Reset做用是關閉旋轉門,也就是不管此時是否已經set過,都將阻塞下一次WaitOne——它應該是開着的。
AutoResetEvent能夠經過2種方式建立,第一種是經過構造函數:
1
|
EventWaitHandle wh =
new
AutoResetEvent (
false
);
|
若是布爾參數爲真,Set方法在構造後馬上被自動的調用,也就是說第一個WaitOne會被放行,不會被阻塞,另外一個方法是經過它的基類EventWaitHandle:
1
|
EventWaitHandle wh =
new
EventWaitHandle (
false
, EventResetMode.Auto);
|
EventWaitHandle的構造器也容許建立ManualResetEvent(用EventResetMode.Manual定義).
在Wait Handle不在須要時候,你應當調用Close方法來釋放操做系統資源。可是,若是一個Wait Handle將被用於程序(就像這一節的大多例子同樣)的生命週期中,你能夠發點懶省略這個步驟,它將在程序域銷燬時自動的被銷燬。
接下來這個例子,一個線程開始等待直到另外一個線程發出信號。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
BasicWaitHandle {
static
EventWaitHandle wh =
new
AutoResetEvent (
false
);
static
void
Main() {
new
Thread (Waiter).Start();
Thread.Sleep (1000);
// 等一會...
wh.Set();
// OK ——喚醒它
}
static
void
Waiter() {
Console.WriteLine (
"Waiting..."
);
wh.WaitOne();
// 等待通知
Console.WriteLine (
"Notified"
);
}
}
Waiting... (pause) Notified.
|
EventWaitHandle的構造器容許以「命名」的方式進行建立,它有能力跨多個進程。名稱是個簡單的字符串,可能會無心地與別的衝突!若是名字使用了,你將引用相同潛在的EventWaitHandle,除非操做系統建立一個新的,看這個例子:
1
2
|
EventWaitHandle wh =
new
EventWaitHandle (
false
, EventResetMode.Auto,
"MyCompany.MyApp.SomeName"
);
|
若是有兩個程序都運行這段代碼,他們將彼此能夠發送信號,等待句柄能夠跨這兩個進程中的全部線程。
設想咱們但願在後臺完成任務,但又不在每次咱們獲得任務時再建立一個新的線程。咱們能夠經過一個輪詢的線程來完成:等待一個任務,執行它,而後等待下一個任務。這是一個廣泛的多線程方案。也就是在建立線程上切份內務操做,任務執行被序列化,在多個工做線程和過多的資源消耗間排除潛在的不想要的操做。 咱們必須決定要作什麼,可是,若是當新的任務來到的時候,工做線程已經在忙以前的任務了,設想這種情形下咱們需選擇阻止調用者直到以前的任務被完成。像這樣的系統能夠用兩個AutoResetEvent對象實現:一個「ready」AutoResetEvent,當準備好的時候,它被工做線程調用Set方法;和「go」AutoResetEvent,當有新任務的時候,它被調用線程調用Set方法。在下面的例子中,一個簡單的string字段被用於決定任務(使用了volatile 關鍵字聲明,來確保兩個線程均可以看到相同版本):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
class
AcknowledgedWaitHandle {
static
EventWaitHandle ready =
new
AutoResetEvent (
false
);
static
EventWaitHandle go =
new
AutoResetEvent (
false
);
static
volatile
string
task;
static
void
Main() {
new
Thread (Work).Start();
// Signal the worker 5 times
for
(
int
i = 1; i <= 5; i++) {
ready.WaitOne();
// First wait until worker is ready
task =
"a"
.PadRight (i,
'h'
);
// Assign a task
go.Set();
// Tell worker to go!
}
// Tell the worker to end using a null-task
ready.WaitOne(); task =
null
; go.Set();
}
static
void
Work() {
while
(
true
) {
ready.Set();
// Indicate that we're ready
go.WaitOne();
// Wait to be kicked off...
if
(task ==
null
)
return
;
// Gracefully exit
Console.WriteLine (task);
}
}
}
ah
ahh
ahhh
ahhhh
|
注意咱們要給task賦null來告訴工做線程退出。在工做線程上調用Interrupt 或Abort 效果是同樣的,假若咱們先調用ready.WaitOne的話。由於在調用ready.WaitOne後咱們就知道工做線程的確切位置,不是在就是剛剛在go.WaitOne語句以前,所以避免了中斷任意代碼的複雜性。調用 Interrupt 或 Abort須要咱們在工做線程中捕捉異常。
另外一個廣泛的線程方案是在後臺工做進程從隊列中分配任務。這叫作生產者/消費者隊列:在工做線程中生產者入列任務,消費者出列任務。這和上個例子很像,除了當工做線程正忙於一個任務時調用者沒有被阻止以外。
生產者/消費者隊列是可縮放的,由於多個消費者可能被建立——每一個都服務於相同的隊列,但開啓了一個分離的線程。這是一個很好的方式利用多處理器的系統來限制工做線程的數量一直避免了極大的併發線程的缺陷(過多的內容切換和資源鏈接)。
在下面例子裏,一個單獨的AutoResetEvent被用於通知工做線程,它只有在用完任務時(隊列爲空)等待。一個通用的集合類被用於隊列,必須經過鎖
控制它的訪問以確保線程安全。工做線程在隊列爲null任務時結束:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
using
System;
using
System.Threading;
using
System.Collections.Generic;
class
ProducerConsumerQueue : IDisposable {
EventWaitHandle wh =
new
AutoResetEvent (
false
);
Thread worker;
object
locker =
new
object
();
Queue<
string
> tasks =
new
Queue<
string
>();
public
ProducerConsumerQueue() {
worker =
new
Thread (Work);
worker.Start();
}
public
void
EnqueueTask (
string
task) {
lock
(locker) tasks.Enqueue (task);
wh.Set();
}
public
void
Dispose() {
EnqueueTask (
null
);
// Signal the consumer to exit.
worker.Join();
// Wait for the consumer's thread to finish.
wh.Close();
// Release any OS resources.
}
void
Work() {
while
(
true
) {
string
task =
null
;
lock
(locker)
if
(tasks.Count > 0) {
task = tasks.Dequeue();
if
(task ==
null
)
return
;
}
if
(task !=
null
) {
Console.WriteLine (
"Performing task: "
+ task);
Thread.Sleep (1000);
// simulate work...
}
else
wh.WaitOne();
// No more tasks - wait for a signal
}
}
}
Here's a main method to test the queue:
class
Test {
static
void
Main() {
using
(ProducerConsumerQueue q =
new
ProducerConsumerQueue()) {
q.EnqueueTask (
"Hello"
);
for
(
int
i = 0; i < 10; i++) q.EnqueueTask (
"Say "
+ i);
q.EnqueueTask (
"Goodbye!"
);
}
// Exiting the using statement calls q's Dispose method, which
// enqueues a null task and waits until the consumer finishes.
}
}
|
Performing task: Hello
Performing task: Say 1
Performing task: Say 2
Performing task: Say 3
...
...
Performing task: Say 9
Goodbye!
注意咱們明確的關閉了Wait Handle在ProducerConsumerQueue被銷燬的時候,由於在程序的生命週期中咱們可能潛在地建立和銷燬許多這個類的實例。
5.4 ManualResetEvent
ManualResetEvent是AutoResetEvent變化的一種形式,它的不一樣之處在於:在線程被WaitOne的調用而經過的時候,它不會自動地reset,這個過程就像大門同樣——調用Set打開門,容許任何數量的已執行WaitOne的線程經過;調用Reset關閉大門,可能會引發一系列的「等待者」直到下次門打開。
你能夠用一個布爾字段"gateOpen" (用 volatile 關鍵字來聲明)與"spin-sleeping" – 方式結合——重複地檢查標誌,而後讓線程休眠一段時間的方式,來模擬這個過程。
ManualResetEvent有時被用於給一個完成的操做發送信號,又或者一個已初始化正準備執行工做的線程。
Mutex提供了與C#的lock語句一樣的功能,這使它大多時候變得的冗餘了。它的優點在於它能夠跨進程工做——提供了一計算機範圍的鎖而勝於程序範圍的鎖。
Mutex是至關快的,而lock 又要比它快上數百倍,獲取Mutex須要花費幾微秒,獲取lock需花費數十納秒(假定沒有阻止)。
對於一個Mutex類,WaitOne獲取互斥鎖,當被搶佔後時發生阻止。互斥鎖在執行了ReleaseMutex以後被釋放,就像C#的lock語句同樣,Mutex只
能從獲取互斥鎖的這個線程上被釋放。
Mutex在跨進程的廣泛用處是確保在同一時刻只有一個程序的的實例在運行,下面演示如何使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class
OneAtATimePlease {
// Use a name unique to the application (eg include your company URL)
static
Mutex mutex =
new
Mutex (
false
,
"oreilly.com OneAtATimeDemo"
);
static
void
Main() {
// Wait 5 seconds if contended – in case another instance
// of the program is in the process of shutting down.
if
(!mutex.WaitOne (TimeSpan.FromSeconds (5),
false
)) {
Console.WriteLine (
"Another instance of the app is running. Bye!"
);
return
;
}
try
{
Console.WriteLine (
"Running - press Enter to exit"
);
Console.ReadLine();
}
finally
{ mutex.ReleaseMutex(); }
}
}
|
Mutex有個好的特性是,若是程序結束時而互斥鎖沒經過ReleaseMutex首先被釋放,CLR將自動地釋放Mutex。
Semaphore就像一個夜總會:它有固定的容量,這由保鏢來保證,一旦它滿了就沒有任何人能夠再進入這個夜總會,而且在其外會造成一個隊列。而後,當人一我的離開時,隊列頭的人即可以進入了。構造器須要至少兩個參數——夜總會的活動的空間,和夜總會的容量。
Semaphore 的特性與Mutex 和 lock有點相似,除了Semaphore沒有「全部者」——它是不可知線程的,任何在Semaphore內的線程均可以調用Release,而Mutex 和 lock僅有那些獲取了資源的線程才能夠釋放它。
在下面的例子中,10個線程執行一個循環,在中間使用Sleep語句。Semaphore確保每次只有不超過3個線程能夠執行Sleep語句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
SemaphoreTest {
static
Semaphore s =
new
Semaphore (3, 3);
// Available=3; Capacity=3
static
void
Main() {
for
(
int
i = 0; i < 10; i++)
new
Thread (Go).Start();
}
static
void
Go() {
while
(
true
) {
s.WaitOne();
Thread.Sleep (100);
// Only 3 threads can get here at once
s.Release();
}
}
}
|
5.7 WaitAny, WaitAll 和 SignalAndWait
除了Set 和 WaitOne方法外,在類WaitHandle中還有一些用來建立複雜的同步過程的靜態方法。
WaitAny, WaitAll 和 SignalAndWait使跨多個可能爲不一樣類型的等待句柄變得容易。
SignalAndWait多是最有用的了:他在某個WaitHandle上調用WaitOne,並在另外一個WaitHandle上自動地調用Set。你能夠在一對EventWaitHandle上裝配兩個線程,而讓它們在某個時間點「相遇」,這馬馬虎虎地合乎規範。AutoResetEvent 或 ManualResetEvent都沒法使用這個技巧。第一個線程像這樣:
WaitHandle.SignalAndWait (wh1, wh2);
同時第二個線程作相反的事情:
WaitHandle.SignalAndWait (wh2, wh1);
WaitHandle.WaitAny等待一組等待句柄任意一個發出信號,WaitHandle.WaitAll等待全部給定的句柄發出信號。與票據旋轉門的例子相似,這些方法可能同時地等待全部的旋轉門——經過在第一個打開的時候(WaitAny狀況下),或者等待直到它們全部的都打開(WaitAll狀況下)。
WaitAll 其實是不肯定的值,由於這與單元模式線程——從COM體系遺留下來的問題,有着奇怪的聯繫。WaitAll 要求調用者是一個多線程單元——剛巧是單元模式最適合——尤爲是在 Windows Forms程序中,須要執行任務像與剪切板結合同樣庸俗!
幸運地是,在等待句柄難使用或不適合的時候,.NET framework提供了更先進的信號結構——Monitor.Wait 和 Monitor.Pulse。
與手工的鎖定相比,你能夠進行說明性的鎖定,用衍生自ContextBoundObject 並標以Synchronization特性的類,
它告訴CLR自動執行鎖操做,看這個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
using
System;
using
System.Threading;
using
System.Runtime.Remoting.Contexts;
[Synchronization]
public
class
AutoLock : ContextBoundObject {
public
void
Demo() {
Console.Write (
"Start..."
);
Thread.Sleep (1000);
// We can't be preempted here
Console.WriteLine (
"end"
);
// thanks to automatic locking!
}
}
public
class
Test {
public
static
void
Main() {
AutoLock safeInstance =
new
AutoLock();
new
Thread (safeInstance.Demo).Start();
// Call the Demo
new
Thread (safeInstance.Demo).Start();
// method 3 times
safeInstance.Demo();
// concurrently.
}
}
|
Start... end
Start... end
Start... end
CLR確保了同一時刻只有一個線程能夠執行 safeInstance中的代碼。它建立了一個同步對象來完成工做,並在每次調
用safeInstance的方法和屬性時在其周圍只可以行鎖定。鎖的做用域——這裏是safeInstance對象,被稱爲同步環境。
那麼,它是如何工做的呢?Synchronization特性的命名空間:System.Runtime.Remoting.Contexts是一個線索。
ContextBoundObject能夠被認爲是一個「遠程」對象,這意味着全部方法的調用是被監聽的。讓這個監聽稱爲可能,
就像咱們的例子AutoLock,CLR自動的返回了一個具備相同方法和屬性的AutoLock對象的代理對象,它扮演着一箇中間
者的角色。總的來講,監聽在每一個方法調用時增長了數微秒的時間。
自動同步不能用於靜態類型的成員,和非繼承自 ContextBoundObject(例如:Windows Form)的類。
鎖在內部以相同的方式運做,你可能期待下面的例子與以前的有同樣的結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
[Synchronization]
public
class
AutoLock : ContextBoundObject {
public
void
Demo() {
Console.Write (
"Start..."
);
Thread.Sleep (1000);
Console.WriteLine (
"end"
);
}
public
void
Test() {
new
Thread (Demo).Start();
new
Thread (Demo).Start();
new
Thread (Demo).Start();
Console.ReadLine();
}
public
static
void
Main() {
new
AutoLock().Test();
}
}
|
(注意咱們放入了Console.ReadLine語句。)由於在同一時刻的同一個此類的對象中只有一個線程能夠執行代碼,
三個新線程將保持被阻止在Demo 放中,直到Test 方法完成,須要等待ReadLine來完成。所以咱們以與以前的有相同
結果而了結,可是隻有在按完Enter鍵以後。這是一個線程安全的手段,差很少足夠能在類中排除任何有用的多線程!
此外,咱們仍未解決以前描述的一個問題:若是AutoLock是一個集合類,好比說,咱們仍然須要一個像下面同樣的鎖,
假設運行在另外一個類裏:
if (safeInstance.Count > 0) safeInstance.RemoveAt (0);
除非使用這代碼的類自己是一個同步的ContextBoundObject!
同步環境能夠擴展到超過一個單獨對象的區域。默認地,若是一個同步對象被實例化從在另外一段代碼以內,它們擁有
共享相同的同步環境(換言之,一個大鎖!)。這個行爲能夠由改變Synchronization特性的構造器的參數來指定。使用
SynchronizationAttribute類定義的常量之一:
常量 |
含義 |
NOT_SUPPORTED |
至關於不使用同步特性 |
SUPPORTED |
若是從另外一個同步對象被實例化,則合併已存在的同步環境,不然只剩下非同步。 |
REQUIRED |
若是從另外一個同步對象被實例化,則合併已存在的同步環境,不然建立一個新的同步環境。 |
REQUIRES_NEW |
老是建立新的同步環境 |
因此若是SynchronizedA的實例被實例化於SynchronizedB的對象中,若是SynchronizedB像下面這樣聲明的話,
它們將有分離的同步環境:
[Synchronization (SynchronizationAttribute.REQUIRES_NEW)]
public class SynchronizedB : ContextBoundObject { ...
越大的同步環境越容易管理,可是減小機會對有用的併發。換個有限的角度,分離的同步環境會形成死鎖,看這個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
[Synchronization]
public
class
Deadlock : ContextBoundObject {
public
DeadLock Other;
public
void
Demo() { Thread.Sleep (1000); Other.Hello(); }
void
Hello() { Console.WriteLine (
"hello"
); }
}
public
class
Test {
static
void
Main() {
Deadlock dead1 =
new
Deadlock();
Deadlock dead2 =
new
Deadlock();
dead1.Other = dead2;
dead2.Other = dead1;
new
Thread (dead1.Demo).Start();
dead2.Demo();
}
}
|
由於每一個Deadlock的實例在Test內建立——一個非同步類,每一個實例將有它本身的同步環境,所以,有它本身的鎖。
當它們彼此調用的時候,不會花太多時間就會死鎖(確切的說是一秒!)。若是Deadlock 和 Test是由不一樣開發團隊來
寫的,這個問題特別容易發生。別期望Test知道如何產生的錯誤,更別期望他們來解決它了。在死鎖顯而易見的狀況下,
這與使用明確的鎖的方式造成鮮明的對比。
6.1 可重入性問題
線程安全方法有時候也被稱爲可重入式的,由於在它執行的時候能夠被搶佔部分線路,在另外的線程調用也不會帶來壞效果。從某個意義上講,術語線程安全和 可重入式的是同義的或者是貼義的。
不過在自動鎖方式上,若是Synchronization的參數可重入式的 爲true的話,可重入性會有潛在的問題:
[Synchronization(true)]
同步環境的鎖在執行離開上下文時被臨時地釋放。在以前的例子裏,這將能預防死鎖的發生;很明顯很須要這樣的功能。然而一個反作用是,在這期間,任何線程均可以自由的調用在目標對象(「重進入」的同步上下文)的上任何方法,而很是複雜的多線程中試圖避免不釋放資源是排在首位的。這就是可重入性的問題。 由於[Synchronization(true)]做用於類級別,這特性打開了對於非上下文的方法訪問,因爲可重入性問題使它們混入類的調用。
雖然可重入性是危險的,但有些時候它是不錯的選擇。好比:設想一個在其內部實現多線程同步的類,將邏輯工做線程運行在不一樣的語境中。在沒有可重入性問題的狀況下,工做線程在它們彼此之間或目標對象之間可能被無理地阻礙。
這凸顯了自動同步的一個基本弱點:超過適用的大範圍的鎖定帶來了其它狀況沒有帶來的巨大麻煩。這些困難:死鎖,可重入性問題和被閹割的併發,使另外一個更簡單的方案——手動的鎖定變得更爲合適
單元模式線程是一個自動線程安全機制, 很是貼近於COM——Microsoft的遺留下的組件對象模型。儘管.NET最大地放棄擺脫了遺留下的模型,但不少時候它也會忽然出現,這是由於有必要與舊的API 進行通訊。單元模式線程與Windows Forms最相關,由於大多Windows Forms使用或包裝了長期存在的Win32 API——連同它的單元傳統。
單元是多線程的邏輯上的「容器」,單元產生兩種容量——「單的」和「多的」。單線 程單元只包含一個線程;多線程單元能夠包含任何數量的線程。單線程模式更廣泛 而且能與二者有互操做性。
就像包含線程同樣,單元也包含對象,當對象在一個單元內被建立後,在它的生命週期中它將一直存在在那,永遠也「居家不出」地與那些駐留線程在一塊兒。這相似於被包含在.NET 同步環境中 ,除了同步環境中沒有本身的或包含線程。任何線程能夠訪問在任何同步環境中的對象 ——在排它鎖的控制中。可是單元內的對象只有單元內的線程才能夠訪問。
想象一個圖書館,每本書都象徵着一個對象;借出書是不被容許的,書都在圖書館 建立並直到它壽終正寢。此外,咱們用一我的來象徵一個線程。
一個同步內容的圖書館容許任何人進入,同時同一時刻只容許一我的進入,在圖書館外會造成隊列。
單元模式的圖書館有常駐維護人員——對於單線程模式的圖書館有一個圖書管理員, 對於多線程模式的圖書館則有一個團隊的管理員。沒人被容許除了隸屬與維護人員的人 ——資助人想要完成研究就必須給圖書管理員發信號,而後告訴管理員去作工做!給管理員發信號被稱爲調度編組——資助人經過調度把方法依次讀出給一個隸屬管理員的人(或,某個隸屬管理員的人!)。 調度編組是自動的,在Windows Forms經過信息泵被實如今庫結尾。這就是操做系統常常檢查鍵盤和鼠標的機制。若是信息到達的太快了,以至不能被處理,它們將造成消息隊列,因此它們能夠以它們到達的順序被處理。
1.1 定義單元模式
.NET線程在進入單元核心Win32或舊的COM代碼前自動地給單元賦值,它被默認地指定爲多線程單元模式,除非須要一個單線程單元模式,就像下面的同樣:
1
2
|
Thread t =
new
Thread (...);
t.SetApartmentState (ApartmentState.STA);
|
你也能夠用STAThread特性標在主線程上來讓它與單線程單元相結合:
1
2
3
4
|
class
Program {
[STAThread]
static
void
Main() {
...
|
線程單元設置對純.NET代碼沒有效果,換言之,即便兩個線程都有STA 的單元狀態,也能夠被相同的對象同時調用相同的方法,就沒有自動的信號編組或鎖定發生了, 只有在執行非託管的代碼時,這纔會發生。
在System.Windows.Forms名稱空間下的類型,普遍地調用Win32代碼, 在單線程單元下工做。因爲這個緣由,一個Windos Forms程序應該在它的主方法上貼上 [STAThread]特性,除非在執行Win32 UI代碼以前如下兩者之一發生了:
在多線程的Windows Forms程序中,經過非建立控件的線程調用控件的的屬性和方法是非法的。全部跨進程的調用必須被明確地排列至建立控件的線程中(一般爲主線程),利用Control.Invoke 或 Control.BeginInvoke方法。你不能依賴自動調度編組由於它發生的太晚了,僅當執行恰好進入了非託管的代碼它才發生,而.NET已有足夠的時間來運行「錯誤的」線程代碼,那些非線程安全的代碼。
一個優秀的管理Windows Forms程序的方案是使用BackgroundWorker, 這個類包裝了須要報道進度和完成度的工做線程,並自動地調用Control.Invoke方法做爲須要。
BackgroundWorker是一個在System.ComponentModel命名空間 下幫助類,它管理着工做線程。它提供瞭如下特性:
最後兩個特性是至關地有用:意味着你再也不須要將try/catch語句塊放到 你的工做線程中了,而且更新Windows Forms控件不須要調用 Control.Invoke了。BackgroundWorker使用線程池工做, 對於每一個新任務,它循環使用避免線程們獲得休息。這意味着你不能在 BackgroundWorker線程上調用 Abort了。
下面是使用BackgroundWorker最少的步驟:
這就設置好了它,任何被傳入RunWorkerAsync的參數將經過事件參數的Argument屬性,傳到DoWork事件委託的方法中,下面是例子:
1
2
3
4
5
6
7
8
9
10
11
12
|
class
Program {
s tatic BackgroundWorker bw =
new
BackgroundWorker();
static
void
Main() {
bw.DoWork += bw_DoWork;
bw.RunWorkerAsync (
"Message to worker"
);
Console.ReadLine();
}
static
void
bw_DoWork (
object
sender, DoWorkEventArgs e) {
// 這被工做線程調用
Console.WriteLine (e.Argument);
// 寫"Message to worker"
// 執行耗時的任務...
}
|
BackgroundWorker也提供了RunWorkerCompleted事件,它在DoWork事件完成後觸發,處理RunWorkerCompleted事件並非強制的,可是爲了查詢到DoWork中的異常,你一般會這麼作的。RunWorkerCompleted中的代碼能夠更新Windows Forms 控件,而不用顯示的信號編組,而DoWork中就能夠這麼作。
添加進程報告支持:
ProgressChanged中的代碼就像RunWorkerCompleted同樣能夠自由地與UI控件進行交互,這在更性進度欄尤其有用。
添加退出報告支持:
下面的例子實現了上面描述的特性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
using
System;
using
System.Threading;
using
System.ComponentModel;
class
Program {
static
BackgroundWorker bw;
static
void
Main() {
bw =
new
BackgroundWorker();
bw.WorkerReportsProgress =
true
;
bw.WorkerSupportsCancellation =
true
;
bw.DoWork += bw_DoWork;
bw.ProgressChanged += bw_ProgressChanged;
bw.RunWorkerCompleted += bw_RunWorkerCompleted;
bw.RunWorkerAsync (
"Hello to worker"
);
Console.WriteLine (
"Press Enter in the next 5 seconds to cancel"
);
Console.ReadLine();
if
(bw.IsBusy) bw.CancelAsync();
Console.ReadLine();
}
static
void
bw_DoWork (
object
sender, DoWorkEventArgs e) {
for
(
int
i = 0; i <= 100; i += 20) {
if
(bw.CancellationPending) {
e.Cancel =
true
;
return
;
}
bw.ReportProgress (i);
Thread.Sleep (1000);
}
e.Result = 123;
// This gets passed to RunWorkerCompleted
}
static
void
bw_RunWorkerCompleted (
object
sender,
RunWorkerCompletedEventArgs e) {
if
(e.Cancelled)
Console.WriteLine (
"You cancelled!"
);
else
if
(e.Error !=
null
)
Console.WriteLine (
"Worker exception: "
+ e.Error.ToString());
else
Console.WriteLine (
"Complete - "
+ e.Result);
// from DoWork
}
static
void
bw_ProgressChanged (
object
sender,
ProgressChangedEventArgs e) {
Console.WriteLine (
"Reached "
+ e.ProgressPercentage +
"%"
);
}
}
|
1.4 BackgroundWorker的子類
BackgroundWorker不是密封類,它提供OnDoWork爲虛方法,暗示着另外一個模式能夠它。 當寫一個可能耗時的方法,你能夠或最好寫個返回BackgroundWorker子類的等方法,預配置完成異步的工做。使用者只要處理RunWorkerCompleted事件和ProgressChanged事件。好比,設想咱們寫一個耗時 的方法叫作GetFinancialTotals:
1
2
3
4
5
|
public
class
Client {
Dictionary <
string
,
int
> GetFinancialTotals (
int
foo,
int
bar) { ... }
...
}
|
咱們能夠如此來實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public
class
Client {
public
FinancialWorker GetFinancialTotalsBackground (
int
foo,
int
bar) {
return
new
FinancialWorker (foo, bar);
}
}
public
class
FinancialWorker : BackgroundWorker {
public
Dictionary <
string
,
int
> Result;
// We can add typed fields.
public
volatile
int
Foo, Bar;
// We could even expose them
// via properties with locks!
public
FinancialWorker() {
WorkerReportsProgress =
true
;
WorkerSupportsCancellation =
true
;
}
public
FinancialWorker (
int
foo,
int
bar) :
this
() {
this
.Foo = foo;
this
.Bar = bar;
}
protected
override
void
OnDoWork (DoWorkEventArgs e) {
ReportProgress (0,
"Working hard on this report..."
);
Initialize financial report data
while
(!finished report ) {
if
(CancellationPending) {
e.Cancel =
true
;
return
;
}
Perform another calculation step
ReportProgress (percentCompleteCalc,
"Getting there..."
);
}
ReportProgress (100,
"Done!"
);
e.Result = Result = completed report data;
}
}
|
不管誰調用GetFinancialTotalsBackground都會獲得一個FinancialWorker——一個用真實地可用地包裝了管理後臺操做。它能夠報告進度,被取消,與Windows Forms交互而不用使用Control.Invoke。它也有異常句柄,而且使用了標準的協議(與使用BackgroundWorker沒任何區別!)
這種BackgroundWorker的用法有效地迴避了舊有的「基於事件的異步模式」。
//注意還有一個老的ReaderWriterLock類,Slim類爲.net 3.5新增,提升了性能。
一般來說,一個類型的實例對於並行的讀操做是線程安全的,可是並行地更新操做則不是(並行地讀與更新也不是)。 這對於資源(好比一個文件)也是同樣的。使用一個簡單的獨佔鎖來鎖定全部可能的訪問可以解決實例的線程安全爲問題,可是當有不少的讀操做而只是偶然的更新操做的時候,這就很不合理的限制了併發。一個例子就是這在一個業務程序服務器中,爲了快速查找把數據緩存到靜態字段中。在這樣的狀況下,ReaderWriterLockSlim類被設計成提供最大可能的鎖定。
ReaderWriterLockSlim有兩種基本的Lock方法:一個獨佔的Wirte Lock ,和一個與其餘Read lock相容的讀鎖定。
因此,當一個線程擁有一個Write Lock的時候,會阻塞全部其餘線程得到讀寫鎖。可是當沒有線程得到WriteLock時,能夠有多個線程同時得到ReadLock,進行讀操做。
ReaderWriterLockSlim提供了下面四個方法來獲得和釋放讀寫鎖:
1
2
3
4
|
public
void
EnterReadLock();
public
void
ExitReadLock();
public
void
EnterWriteLock();
public
void
ExitWriteLock();
|
另外對於全部的EnterXXX方法,還有」Try」版本的方法,它們接收timeOut參數,就像Monitor.TryEnter同樣(在資源爭用嚴重的時候超時發生至關容易)。另外ReaderWriterLock提供了其餘相似的AcquireXXX 和 ReleaseXXX方法,它們超時退出的時候拋出異常而不是返回false。
下面的程序展現了ReaderWriterLockSlim——三個線程循環地枚舉一個List,同時另外兩個線程每一秒鐘添加一個隨機數到List中。一個read lock保護List的讀取線程,同時一個write lock保護寫線程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
class
SlimDemo
{
static
ReaderWriterLockSlim rw =
new
ReaderWriterLockSlim();
static
List<
int
> items =
new
List<
int
>();
static
Random rand =
new
Random();
static
void
Main()
{
new
Thread (Read).Start();
new
Thread (Read).Start();
new
Thread (Read).Start();
new
Thread (Write).Start (
"A"
);
new
Thread (Write).Start (
"B"
);
}
static
void
Read()
{
while
(
true
)
{
rw.EnterReadLock();
foreach
(
int
i
in
items) Thread.Sleep (10);
rw.ExitReadLock();
}
}
static
void
Write (
object
threadID)
{
while
(
true
)
{
int
newNumber = GetRandNum (100);
rw.EnterWriteLock();
items.Add (newNumber);
rw.ExitWriteLock();
Console.WriteLine (
"Thread "
+ threadID +
" added "
+ newNumber);
Thread.Sleep (100);
}
}
static
int
GetRandNum (
int
max) {
lock
(rand)
return
rand.Next (max); }
}
<em><span style=
"font-family: YaHei Consolas Hybrid;"
>
//在實際的代碼中添加try/finally,保證異常狀況寫lock也會被釋放。</span></em>
|
結果爲:
Thread B added 61 Thread A added 83 Thread B added 55 Thread A added 33 ...
ReaderWriterLockSlim比簡單的Lock容許更大的併發讀能力。咱們可以添加一行代碼到Write方法,在While循環的開始:
1
|
Console.WriteLine (rw.CurrentReadCount +
" concurrent readers"
);
|
基本上老是會返回「3 concurrent readers」(讀方法花費了更多的時間在Foreach循環),ReaderWriterLockSlim還提供了許多與CurrentReadCount屬性相似的屬性來監視lock的狀況:
1
2
3
4
5
6
7
8
9
10
11
|
public
bool
IsReadLockHeld {
get
; }
public
bool
IsUpgradeableReadLockHeld {
get
; }
public
bool
IsWriteLockHeld {
get
; }
public
int
WaitingReadCount {
get
; }
public
int
WaitingUpgradeCount {
get
; }
public
int
WaitingWriteCount {
get
; }
public
int
RecursiveReadCount {
get
; }
public
int
RecursiveUpgradeCount {
get
; }
public
int
RecursiveWriteCount {
get
; }
|
有時候,在一個原子操做裏面交換讀寫鎖是很是有用的,好比,當某個item不在list中的時候,添加此item進去。最好的狀況是,最小化寫如鎖的時間,例如像下面這樣處理:
1 得到一個讀取鎖
2 測試list是否包含item,若是是,則返回
3 釋放讀取鎖
4 得到一個寫入鎖
5 寫入item到list中,釋放寫入鎖。
可是在步驟三、4之間,當另一個線程可能偷偷修改List(好比說添加一樣一個Item),ReaderWriterLockSlim經過提供第三種鎖來解決這個問題,這就是upgradeable lock。一個可升級鎖和read lock 相似,只是它可以經過一個原子操做,被提高爲write lock。使用方法以下:
從調用者的角度,這很是想遞歸(嵌套)鎖。實際上第三步的時候,經過一個原子操做,釋放了read lock 並得到了一個新的write lock.
upgradeable locks 和read locks之間另外還有一個重要的區別,儘管一個upgradeable locks 可以和任意多個read locks共存,可是一個時刻,只能有一個upgradeable lock本身被使用。這防止了死鎖。這和SQL Server的Update lock相似
咱們能夠改變前面例子的Write方法來展現upgradeable lock:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
while
(
true
)
{
int
newNumber = GetRandNum (100);
rw.EnterUpgradeableReadLock();
if
(!items.Contains (newNumber))
{
rw.EnterWriteLock();
items.Add (newNumber);
rw.ExitWriteLock();
Console.WriteLine (
"Thread "
+ threadID +
" added "
+ newNumber);
}
rw.ExitUpgradeableReadLock();
Thread.Sleep (100);
}
|
ReaderWriterLock 沒有提供upgradeable locks的功能。
2.1 遞歸鎖 Lock recursion
Ordinarily, nested or recursive locking is prohibited with ReaderWriterLockSlim. Hence, the following throws an exception:
默認狀況下,遞歸(嵌入)鎖被ReaderWriterLockSlim禁止,由於下面的代碼可能拋出異常。
1
2
3
4
5
|
var
rw =
new
ReaderWriterLockSlim();
rw.EnterReadLock();
rw.EnterReadLock();
rw.ExitReadLock();
rw.ExitReadLock();
|
可是顯示地聲明容許嵌套的話,就能正常工做,不過這帶來了沒必要要的複雜性。
1
|
var
rw =
new
ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion);
|
1
2
3
4
5
6
|
rw.EnterWriteLock();
rw.EnterReadLock();
Console.WriteLine (rw.IsReadLockHeld);
// True
Console.WriteLine (rw.IsWriteLockHeld);
// True
rw.ExitReadLock();
rw.ExitWriteLock();
|
使用鎖的順序大體爲:Read Lock --> Upgradeable Lock --> Write Lock
若是你的程序有不少線程,致使花費了大多時間在等待句柄的阻止上,你能夠經過 線程池來削減負擔。線程池經過合併不少等待句柄在不多的線程上來節省時間。
使用線程池,你須要註冊一個連同將被執行的委託的Wait Handle,在Wait Handle發信號時。這個工做經過調用ThreadPool.RegisterWaitForSingleObject來完成,以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class
Test {
static
ManualResetEvent starter =
new
ManualResetEvent (
false
);
public
static
void
Main() {
ThreadPool.RegisterWaitForSingleObject (starter, Go,
"hello"
, -1,
true
);
Thread.Sleep (5000);
Console.WriteLine (
"Signaling worker..."
);
starter.Set();
Console.ReadLine();
}
public
static
void
Go (
object
data,
bool
timedOut) {
Console.WriteLine (
"Started "
+ data);
// Perform task...
}
}
|
除了等待句柄和委託以外,RegisterWaitForSingleObject也接收一個「黑盒」對象,它被傳遞到你的委託方法中( 就像用ParameterizedThreadStart同樣),擁有一個毫秒級的超時參數(-1意味着沒有超時)和布爾標誌來指明請求是一次性的仍是循環的。
全部進入線程池的線程都是後臺的線程,這意味着 它們在程序的前臺線程終止後將自動的被終止。但你若是想等待進入線程池的線程都完成它們的重要工做在退出程序以前,在它們上調用Join是不行的,由於進入線程池的線程歷來不會結束!意思是說,它們被改成循環,直到父進程終止後才結束。因此爲知道運行在線程池中的線程是否完成,你必須發信號——好比用另外一個Wait Handle。
在線程池中的線程上調用Abort 是一個壞主意,線程須要在程序域的生命週期中循環。
你也能夠用QueueUserWorkItem方法而不用等待句柄來使用線程池,它定義了一個當即執行的委託。你沒必要在多個任務中節省共享線程,但有一個慣例:線程池保持一個線程總數的封頂(默認爲25),在任務數達到這個頂值後將自動排隊。這就像程序範圍的有25個消費者的生產者/消費者隊列。在下面的例子中,100個任務入列到線程池中,而一次只執行 25個,主線程使用Wait 和 Pulse來等待全部的任務完成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class
Test {
static
object
workerLocker =
new
object
();
static
int
runningWorkers = 100;
public
static
void
Main() {
for
(
int
i = 0; i < 100; i++) {
ThreadPool.QueueUserWorkItem (Go, i);
}
Console.WriteLine (
"Waiting for threads to complete..."
);
lock
(workerLocker) {
while
(runningWorkers > 0) Monitor.Wait (workerLocker);
}
Console.WriteLine (
"Complete!"
);
Console.ReadLine();
}
public
static
void
Go (
object
instance) {
Console.WriteLine (
"Started: "
+ instance);
Thread.Sleep (1000);
Console.WriteLine (
"Ended: "
+ instance);
lock
(workerLocker) {
runningWorkers--; Monitor.Pulse (workerLocker);
}
}
}
|
爲了傳遞多個對象給目標方法,你能夠定義個擁有全部須要屬性的自定義對象,或者調用一個匿名方法。好比若是Go方法接收兩個整型參數,會像下面這樣:
1
|
ThreadPool.QueueUserWorkItem (
delegate
(
object
notUsed) { Go (23,34); });
|
另外一個進入線程池的方式是經過異步委託。
在第一部分咱們描述如何使用 ParameterizedThreadStart把數據傳入線程中。有時候 你須要經過另外一種方式,來從線程中獲得它完成後的返回值。異步委託提供了一個便利的機制,容許許多參數在兩個方向上傳遞 。此外,未處理的異常在異步委託中在原始線程上被從新拋出,所以在工做線程上不須要明確的處理了。異步委託也提供了計入 線程池的另外一種方式。
對此你必須付出的代價是要跟從異步模型。爲了看看這意味着什麼,咱們首先討論更常見的同步模型。咱們假設咱們想比較 兩個web頁面,咱們按順序取得它們,而後像下面這樣比較它們的輸出:
1
2
3
4
5
6
|
static
void
ComparePages() {
WebClient wc =
new
WebClient ();
Console.WriteLine (s1 == s2 ?
"Same"
:
"Different"
);
}
|
若是兩個頁面同時下載固然會更快了。問題在於當頁面正在下載時DownloadString阻止了繼續調用方法。若是咱們能 調用 DownloadString在一個非阻止的異步方式中會變的更好,換言之:
1. 咱們告訴 DownloadString 開始執行
2. 在它執行時咱們執行其它任務,好比說下載另外一個頁面
3. 咱們詢問DownloadString的全部結果
WebClient類實際上提供一個被稱爲DownloadStringAsync的內建方法 ,它提供了就像異步函數的功能。而眼下,咱們忽略這個問題,集中精力在任何方法均可以被異步調用的機制上。
第三步使異步委託變的有用。調用者聚集了工做線程獲得結果和容許任何異常被從新拋出。沒有這步,咱們只有普通多線程。雖然也可能不用聚集方式使用異步委託,你能夠用ThreadPool.QueueWorkerItem 或 BackgroundWorker。
下面咱們用異步委託來下載兩個web頁面,同時實現一個計算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
delegate
string
DownloadString (
string
uri);
static
void
ComparePages() {
// Instantiate delegates with DownloadString's signature:
DownloadString download1 =
new
WebClient().DownloadString;
DownloadString download2 =
new
WebClient().DownloadString;
// Start the downloads:
IAsyncResult cookie1 = download1.BeginInvoke (uri1,
null
,
null
);
IAsyncResult cookie2 = download2.BeginInvoke (uri2,
null
,
null
);
// Perform some random calculation:
double
seed = 1.23;
for
(
int
i = 0; i < 1000000; i++) seed = Math.Sqrt (seed + 1000);
// Get the results of the downloads, waiting for completion if necessary.
// Here's where any exceptions will be thrown:
string
s1 = download1.EndInvoke (cookie1);
string
s2 = download2.EndInvoke (cookie2);
Console.WriteLine (s1 == s2 ?
"Same"
:
"Different"
);
}
|
咱們以聲明和實例化咱們想要異步運行的方法開始。在這個例子中,咱們須要兩個委託,每一個引用不一樣的WebClient的對象(WebClient 不容許並行的訪問,若是它容許,咱們就只需一個委託了)。
咱們而後調用BeginInvoke,這開始執行並馬上返回控制器給調用者。依照咱們的委託,咱們必須傳遞一個字符串給 BeginInvoke (編譯器由生產BeginInvoke 和 EndInvoke在委託類型強迫實現這個).
BeginInvoke 還須要兩個參數:一個可選callback和數據對象;它們一般不須要而被設置爲null, BeginInvoke返回一個 IASynchResult對象,它擔當着調用 EndInvoke所用的數據。IASynchResult 同時有一個IsCompleted屬性來檢查進度。
以後咱們在委託上調用EndInvoke ,獲得須要的結果。若是有必要,EndInvoke會等待, 直到方法完成,而後返回方法返回的值做爲委託指定的(這裏是字符串)。 EndInvoke一個好的特性是DownloadString有任何的引用或輸出參數, 它們會在 EndInvoke結構賦值,容許經過調用者多個值被返回。
在異步方法的執行中的任何點發生了未處理的異常,它會從新在調用線程在EndInvoke中拋出。 這提供了精簡的方式來管理返回給調用者的異常。
若是你異步調用的方法沒有返回值,你也(理論上)應該調用EndInvoke,在部分意義上 在開放了誤判;MSDN上辯論着這個話題。若是你選擇不調用EndInvoke,你須要考慮在工做方法中的異常。
4.1 異步方法
.NET Framework 中的一些類型提供了某些它們方法的異步版本,它們使用"Begin" 和 "End"開頭。它們被稱之爲異步方法,它們有與異步委託相似的特性,但異步委託存在着一些待解決的困難的問題:容許比你所擁有的線程還多的併發活動率。 好比一個web或TCP Socket服務器,若是用NetworkStream.BeginRead 和 NetworkStream.BeginWrite 來寫的話,就可能在僅僅線程池線程中處理數百個併發的請求。
除非你正在寫一個專門的高併發程序,不然不該該過多地使用異步方法。理由以下:
若是你只是像簡單地得到並行執行的結果,你最好遠離調用異步版本的方法(好比NetworkStream.Read) 而經過異步委託。另外一個選項是使用ThreadPool.QueueUserWorkItem或BackgroundWorker,又或者只是簡單地建立新的線程。
4.2 異步事件
另外一種模式存在,就是爲何類型能夠提供異步版本的方法。這就是所謂的「基於事件的異步模式」,這些的方法以"Async"結束,相對應的事件以"Completed"結束。WebClient使用這個模式在它的DownloadStringAsync 方法中。 爲了使用它,你要首先處理"Completed" 事件(例如:DownloadStringCompleted),而後調用"Async"方法(例如:DownloadStringAsync)。當方法完成後,它調用你事件句柄。不幸的是,WebClient的實現是有缺陷的:像DownloadStringAsync 這樣的方法對於下載的一部分時間阻止了調用者的線程。
基於事件的模式也提供了報道進度和取消操做,被友好地設計成可對Windows程序可更新forms和控件。若是在某個類型中你須要這些特性 ,而它卻不支持(或支持的很差)基於事件的模式,你不必去本身實現它(你也根本不想去作!)。儘管如此,全部的這些經過BackgroundWorker這個幫助類即可輕鬆完成。
週期性的執行某個方法最簡單的方法就是使用一個計時器,好比System.Threading 命名空間下Timer類。線程計時器利用了線程池,容許多個計時器被建立而沒有額外的線程開銷。 Timer 算是至關簡易的類,它有一個構造器和兩個方法(這對於極簡主義者來講是最高興不過的了)。
1
2
3
4
5
6
7
8
9
|
public
sealed
class
Timer : MarshalByRefObject, IDisposable
{
public
Timer (TimerCallback tick,
object
state, 1st, subsequent);
public
bool
Change (1st, subsequent);
// To change the interval
public
void
Dispose();
// To kill the timer
}
1st = time to the first tick
in
milliseconds or a TimeSpan
subsequent = subsequent intervals
in
milliseconds or a TimeSpan
(use Timeout.Infinite
for
a one-off callback)
|
接下來這個例子,計時器5秒鐘以後調用了Tick 的方法,它寫"tick...",而後每秒寫一個,直到用戶敲 Enter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
using
System;
using
System.Threading;
class
Program {
static
void
Main() {
Timer tmr =
new
Timer (Tick,
"tick..."
, 5000, 1000);
Console.ReadLine();
tmr.Dispose();
// End the timer
}
static
void
Tick (
object
data) {
// This runs on a pooled thread
Console.WriteLine (data);
// Writes "tick..."
}
}
|
.NET framework在System.Timers命名空間下提供了另外一個計時器類。它徹底包裝自System.Threading.Timer,在使用相同的線程池時提供了額外的便利——相同的底層引擎。下面是增長的特性的摘要:
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
using
System;
using
System.Timers;
// Timers namespace rather than Threading
class
SystemTimer {
static
void
Main() {
Timer tmr =
new
Timer();
// Doesn't require any args
tmr.Interval = 500;
tmr.Elapsed += tmr_Elapsed;
// Uses an event instead of a delegate
tmr.Start();
// Start the timer
Console.ReadLine();
tmr.Stop();
// Pause the timer
Console.ReadLine();
tmr.Start();
// Resume the timer
Console.ReadLine();
tmr.Dispose();
// Permanently stop the timer
}
static
void
tmr_Elapsed (
object
sender, EventArgs e) {
Console.WriteLine (
"Tick"
);
}
}
|
.NET framework 還提供了第三個計時器——在System.Windows.Forms 命名空間下。雖然相似於System.Timers.Timer 的接口,但功能特性上有根本的不一樣。一個Windows Forms 計時器不能使用線程池,代替爲老是在最初建立它的線程上觸發 "Tick"事件。假定這是主線程——負責實例化全部Windows Forms程序中的forms和控件,計時器的事件可以操做forms和控件而不違反線程安全——或者強加單元線程模式。Control.Invoke是不須要的。它實質上是一個單線程timer
Windows Forms計時器必須迅速地執行來更新用戶接口。迅速地執行是很是重要的,由於Tick事件被主線程調用,若是它有停頓, 將使用戶接口變的沒有響應。
每一個線程與其它線程數據存儲是隔離的,這對於「不相干的區域」的存儲是有益的,它支持執行路徑的基礎結構,如通訊,事務和安全令牌。 經過方法參數傳遞這些數據是十分笨拙的。存儲這些數據到靜態域意味着這些數據能夠被全部線程共享。
Thread.GetData從一個線程的隔離數據中讀,Thread.SetData 寫入數據。 兩個方法須要一個LocalDataStoreSlot對象來識別內存槽——這包裝自一個內存槽的名稱的字符串,這個名稱 你能夠跨全部的線程使用,它們將獲得不各自的值,看這個例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class
... {
// 相同的LocalDataStoreSlot 對象能夠用於跨全部線程
LocalDataStoreSlot secSlot = Thread.GetNamedDataSlot (
"securityLevel"
);
// 這個屬性每一個線程有不一樣的值
int
SecurityLevel {
get
{
object
data = Thread.GetData (secSlot);
return
data ==
null
? 0 : (
int
) data;
// null == 未初始化
}
set
{
Thread.SetData (secSlot, value);
}
}
...
|
Thread.FreeNamedDataSlot將釋放給定的數據槽,它跨全部的線程——但只有一次,當全部相同名字LocalDataStoreSlot對象做爲垃圾被回收時退出做用域時發生。這確保了線程不獲得數據槽從它們的腳底下撤出——也保持了引用適當的使用之中的LocalDataStoreSlot對象。