C#中的執行緒(一)入門(認真的帖子必須轉載擴散)
轉載:http://www.cnblogs.com/miniwiki/archive/2010/06/18/1760540.html
文章系參考轉載,英文原文網址請參考:http://www.albahari.com/threading/
作者 Joseph Albahari, 翻譯 Swanky Wu
中文翻譯作者把原文放在了"google 協作"上面,GFW遮蔽,不能訪問和檢視,因此我根據譯文和英文原版整理轉載到園子裡面。
本系列文章可以算是一本很出色的C#執行緒手冊,思路清晰,要點都有介紹,看了後對C#的執行緒及同步等有了更深入的理解。
- 入門
- 執行緒同步基礎
- 同步要領
- 鎖和執行緒安全
- Interrupt 和 Abort
- 執行緒狀態
- 等待控制代碼
- 同步環境
- 使用多執行緒
- 單元模式和Windows Forms
- BackgroundWorker類
- ReaderWriterLock類
- 執行緒池
- 非同步委託
- 計時器
- 區域性儲存
- 高階話題
- 非阻止同步
- Wait和Pulse
- Suspend和Resume
- 終止執行緒
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分配每個執行緒到它自己的記憶體堆疊上,來保證區域性變數的分離執行。在接下來的方法中我們定義了一個區域性變數,然後在主執行緒和新建立的執行緒上同時地呼叫這個方法。
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=
"https://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"
|