1. 程式人生 > >C# 線程入門 00

C# 線程入門 00

屬於 分享 在線 等待 ron n! num 鼠標 線程創建

內容預告:

  • 線程入門(線程概念,創建線程)
  • 同步基礎(同步本質,線程安全,線程中斷,線程狀態,同步上下文)
  • 使用線程(後臺任務,線程池,讀寫鎖,異步代理,定時器,本地存儲)
  • 高級話題(非阻塞線程,扶起和恢復)

概覽:

C#支持通過多線程並行地執行代碼,一個線程是獨立的執行個體,可以和其他線程同時運行。

CLR和操作系統會給C#程序開啟一個線程(主線程),可以被用來作為創建多線程的起點,例子:

技術分享
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
}
}
技術分享

運行結果將是:

技術分享
xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
yyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
技術分享

主線程創建了一個線程 t ,執行了重復輸出y的操作,主要線程執行了重復輸出x的操作。
CLR給每個線程分配了單獨的線程棧,所以本地變量是每個線程單獨保存的,下面的例子,我們用一個本地變量定義一個函數,然後在main函數和新的線程裏同時執行這個函數

技術分享
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 (‘?‘);
}
技術分享

執行結果:

??????????

每個線程的內存棧裏都創建了一個單獨的變量cycle,所以輸出是10個?
如果是引用同一個對象的話,線程則共享這個數據:

按 Ctrl+C 復制代碼 按 Ctrl+C 復制代碼

因為兩個線程都調用Go(),它們共享done這個變量,所以done只輸出一次:

Done

static變量提供一種不同的方式在線程中共享變量,這裏是一個例子:

技術分享
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的次序,

技術分享
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) { Console.WriteLine ("Done"); done = true; }
}
}
技術分享

done就有可能輸出兩次。因為在一個線程計算if表達式然後執行Console.WriteLine ("Done");的時候,另一個線程可能有機會在done的值改變之前先輸出done。
其實在C#中可以用lock來達到這個目的:

技術分享
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; }
}
}
}
技術分享

當兩個線程同時競爭一個鎖時,一個線程等待,或者說阻塞,直到鎖空出來。這主要是保證同時只能有一個線程可以進入臨界代碼區域,"Done"只會被輸出一次。
代碼是以這樣的方式被保護的,來自於多線程上下文的不確定性,叫做線程安全。
臨時地暫停,或阻塞,是線程同步的基本功能。
如果一個線程想要暫停,或者休眠一段時間,可以用:

Thread.Sleep (TimeSpan.FromSeconds (30)); // 阻塞30秒

一個線程可以通過調用Join等待另一個線程結束:

Thread t = new Thread (Go); // 假設Go是靜態函數。
t.Start();
Thread.Join (t); // 阻塞,只到線程t結束。

線程如何工作:
在內部,多線程是被線程調度器管理的,是CLR代替操作系統幹的活。線程調度器要保證所有活躍線程合理分配執行時間,以及在等待中的線程(這些線程是不消耗CPU時間的)。
在單核機器上,線程調度是以在活躍線程間快速切換時間片的方式工作的。就像就第一個例子,重復輸出x或y的線程輪換得到時間片。在Windows XP下,一個時間片就是幾十毫秒,這還是要比CPU在線程間切換能幹更多事,一次線程切換也就幾毫秒的事。
在多核機器上,多線程的實現是結合了時間片輪換和並發,並發是不同的線程同時運行在不同的CPU上,因為機器要運行的線程數遠遠大於CPU的數量,所以還需要時間片切換。
線程不能控制自己什麽時候執行,完全由操作系統的時間片切換機制來控制。

線程和進程:

總是有面試官喜歡把線程和進程做比較,其實兩者根本不是一個級別的東西。一個單獨的應用程序內所有的線程都在邏輯或屬於一個進程的。進程:一個運行應用程序的操作系統單元。線程與進程有些相似之處,比如:對於實例,進程和線程都是典型的時間片輪換的執行機制。關鍵的不同點在於進程間是相互獨立的,而同一個應用程序裏的線程間是共享堆內存的,這也是性能的用武之地:一個線程可以在後臺運行,另一個線程可以顯示得到的數據。

什麽時候應該用多線程:

  • 一個普通的多線程程序在後臺運行耗時的任務時。主線程保持運行狀態,工作線程幹後臺的活。在Windows Form程序裏,如果主線程被長時間占用,鍵盤和鼠標的操作就不能處理了,然後程序就變成“無響應”了。所以,需要把耗時的任務放在後臺運行,讓主線程保證響應用戶輸入。
  • 在非UI程序中,比如Windows服務,多線程就特別有用了,當等待另一臺機器(例如一個應用服務器,數據庫服務器,客戶端)的響應時,用一個工作線程來等待,讓主線程保持暢通。
  • 多線程的另一個用處是在函數中有大量計算時,函數劃成多個線程可以在多核的機器上執行更快(可以用Environment.ProcessorCount得到CPU核心數量)。
  • 一個C#程序可以通過兩種方式成為多線程:顯示地創建線程,或者使用.NET顯示創建線程的功能(比如BackgroundWorker,線程池,定時器,遠程服務器,WebSerivce或ASP.NET程序),在後面這些情況下,只能是多線程。單線程的web服務器肯定不行。在無狀態的web服務器裏,多線程是相當簡單的。主要的問題是如果處理緩存數據的鎖機制。

什麽時候不應該用多線程:

多線程也有缺點,最大的問題是會讓程序變得復雜,多線程本身並不復雜,復雜在於線程間的交互。能讓開發周期變長,以及Bug變多。所以需要把多個線程間的交互設計的盡量簡單,或者就別用多線程,除非你可以保證的很好。

過多地在線程間切換和分配內存棧,也會帶來CPU資源的消耗,通常,當硬盤IO很多時,只有一兩個線程依次執行任務的程序性能要更好,而多個性能同時執行一個任務的性能不怎麽樣。後面會討論生產者/消費者模型。


創建和啟動線程:

可以用Thread類的構造函數創建線程,傳遞一個ThreadStart的代理作為參數,這個代理指向將要執行的函數,以下是這個代理的定義:

public delegate void ThreadStart();

執行Start()函數,線程即開始運行,在函數結束後線程會返回,下面是創建ThreadStart的C#語法:

按 Ctrl+C 復制代碼 按 Ctrl+C 復制代碼

線程t執行Go函數,同時主線程也調用Go,執行結果是:

hello!
hello!

也可以用C#的語法糖:編譯器會自動創建一個ThreadStart的代理。

技術分享
static void Main() {
Thread t = new Thread (Go); // No need to explicitly use ThreadStart
t.Start();
...
}
static void Go() { ... }
技術分享

還有更簡單的匿名函數語法:

static void Main() {
Thread t = new Thread (delegate() { Console.WriteLine ("Hello!"); });
t.Start();
}

給ThreadStart傳遞參數:這種形式只能傳遞一個參數

public delegate void ParameterizedThreadStart (object obj);
技術分享
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!");
}
技術分享

結果:

hello!
HELLO!

如果用匿名函數方式:可以傳遞多個參數,且也不需要類型轉換,

技術分享
static void Main() {
Thread t = new Thread (delegate() { WriteText ("Hello"); });
t.Start();
}
static void WriteText (string text) { Console.WriteLine (text); }
技術分享

還有一種傳參的方式是傳一個實例過去,而不是傳一個靜態函數:

技術分享
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(); // Main thread – runs with upper=false
}
void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); }
技術分享

線程命名:線程有一個Name屬性,在調試時很有用。

技術分享
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);
}
}
技術分享

輸出:

Hello from main
Hello from worker

前臺線程和後臺線程:

默認情況下,線程都是前臺線程,意味著任何一個前臺線程正在運行,程序就是運行的。而後臺線程在所有前臺線程終止時也會立即終止。

把線程從前臺改為後臺,線程在CPU調度器的優先級和狀態是不會改變的。

技術分享
class PriorityTest {
static void Main (string[] args) {
Thread worker = new Thread (delegate() { Console.ReadLine(); });
if (args.Length > 0) 
worker.IsBackground = true; worker.Start(); } }
技術分享

如果這個程序執行時不帶參數,worker線程默認是前臺線程,並且會在ReadLine這一行等著用戶輸入。同時,主線程退出,但是程序會繼續運行,因為ReadLine也是前臺線程。如果傳了一個參數給Main函數,worker線程的狀態則被設置成後臺狀態,程序幾乎會在主線程結束時立即退出--終於ReadLine。當後臺線程以這種方式終止時,任何代碼都不再執行了,這種代碼是不推薦的,所以最好在程序退出前等待所有後臺線程,可以用超時時間(Thread.Join)來做。如果因為某些原因worker線程一直不結束,也能終止這個線程,這種情況下最好記錄一下日誌來分析什麽情況導致的。

在Windows Form中被拋棄的前臺線程是個潛在的危險,因為程序在主線程結束將要退出時,它還在繼續運行。在Windows的任務管理器裏,它在應用程序Tab裏會消失,但在進程Tab裏還在。除非用戶顯式地結束它。

常見的程序退出失敗的可能性就是忘記了前臺線程。


線程的優先級:線程的優先級決定了線程的執行時間。

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

線程的優先級為Highest時,並不意味著線程會實時運行,要想實時運行,進程的優先級也得是High。當你的進程的優先級是High時,如果程序進入了死循環,系統會死鎖。這個時候就只有按電源鍵了。所以,慎用。

最好將實時線程和UI分開在兩個線程,並設置成不同的優先級,通過遠程或共享內存通信,共享內存需要P/Invoking Win32 API(CreateFileMapping和MapViewOfFile)。


線程的異常處理:線程一旦啟動,任何在try/catch/finally範圍內創建線程的代碼塊與try/catch/finally就沒有什麽關系了。

技術分享
public static void Main() {
try {
new Thread (Go).Start();
}
catch (Exception ex) {
// We‘ll never get here!
Console.WriteLine ("Exception!");
}
static void Go() { throw null; }
}
技術分享

上例中的try/catch基本沒用了,新創建的線程可能是未處理的空引用異常,最好在線程要執行的代碼裏加異常捕獲:

技術分享
public static void Main() {
new Thread (Go).Start();
}
static void Go() {
try {
...
throw null; // this exception will get caught below
...
}
catch (Exception ex) {
Typically log the exception, and/or signal another thread
that we‘ve come unstuck
...
}
技術分享

從.NET2.0開始,線程上任何未處理的異常會導致整個程序掛掉,意味著千萬別忽略異常,在線程要執行的函數裏,給每個可能異常的代碼加上try/catch。這可能有點麻煩,所以,很多人這樣處理,用全局的異常處理:

技術分享
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) {
Log exception, then either exit the app or continue...
}
}
技術分享

Application.ThreadException事件會在代碼拋出異常時被觸發,這樣看起來很完美--可以捕獲所有異常,但在worker線程上的異常可能捕獲不了,在main函數裏的窗體的構造函數,在Windows的消息循環之前就執行了。.NET提供了一個低層的事件捕獲全局異常:AppDomain.UnhandledException,它才可以捕獲所有異常(UI和非UI的)。雖然它提供了一個很好的方式捕獲所有異常並記錄異常日誌,但是它沒有辦法阻止程序關系,也沒有辦法阻止.NET的異常對話框

C# 線程入門 00