C# 執行緒相關知識總結
初識執行緒
執行緒是一個獨立的執行單元,每個程序內部都有多個執行緒,每個執行緒都可以各自同時執行指令。每個執行緒都有自己獨立的棧,但是與程序內的其他執行緒共享記憶體。但是對於.NET的客戶端程式(Console,WPF,WinForms)是由CLR建立的單執行緒(主執行緒,且只建立一個執行緒)來啟動。在該執行緒上可以建立其他執行緒。
圖:
執行緒工作方式
多執行緒由內部執行緒排程程式管理,執行緒排程器通常是CLR委派給作業系統的函式。執行緒排程程式確保所有活動執行緒都被分配到合適的執行時間,執行緒在等待或阻止時 (例如,在一個獨佔鎖或使用者輸入) 不會消耗 CPU 時間。
在單處理器計算機上,執行緒排程程式是執行時間切片 — 迅速切換每個活動執行緒。在 Windows 中,一個時間片是通常數十毫秒為單位的區域 — — 相比來說 執行緒間相互切換比CPU更消耗資源。在多處理器計算機上,多執行緒用一種混合的時間切片和真正的併發性來實現,不同的執行緒會在不同的cpu執行程式碼。
建立執行緒
如:
using System; using System.Threading; class ThreadTest { static void Main() { Thread t = new Thread (Write2); // 建立執行緒t t.Start(); // 執行 Write2() // 同時執行主執行緒上的該方法 for (int i = 0; i < 1000; i++) Console.Write ("1"); } static void Write2() { for (int i = 0; i < 1000; i++) Console.Write ("2"); } }
輸出
111122221122221212122221212......
在主執行緒上建立了一個新的執行緒,該新執行緒執行WrWrite2方法,在呼叫t.Start()時,主執行緒並行,輸出“1”。
圖:
執行緒Start()之後,執行緒的IsAlive屬性就為true,直到該執行緒結束(當執行緒傳入的方法結束時,該執行緒就結束)。
CLR使每個執行緒都有自己獨立的記憶體棧,所以每個執行緒的本地變數都相互獨立。
如:
static void Main() { new Thread (Go).Start(); // 建立一個新執行緒,並呼叫Go方法 Go(); // 在主執行緒上呼叫Go方法 } static void Go() { // 宣告一個本地區域性變數 cycles for (int cycles = 0; cycles < 5; cycles++) Console.Write ('N'); }
輸出
NNNNNNNNNN (共輸出10個N)
在新執行緒和主執行緒上呼叫Go方法時分別建立了變數cycles,這時cycles在不同的執行緒棧上,所以相互獨立不受影響。
圖:
如果不同執行緒指向同一個例項的引用,那麼不同的執行緒共享該例項。
如:
class ThreadTest { //全域性變數 int i; static void Main() { ThreadTest tt = new ThreadTest(); // 建立一個ThreadTest類的例項 new Thread (tt.Go).Start(); tt.Go(); } // Go方法屬於ThreadTest的例項 void Go() { if (i==1) { ++i; Console.WriteLine (i); } } }
輸出
2
新執行緒和主執行緒上呼叫了同一個例項的Go方法,所以變數i共享。
靜態變數也可以被多執行緒共享
class ThreadTest { static int i; // 靜態變數可以被執行緒共享 static void Main() { new Thread (Go).Start(); Go(); } static void Go() { if (i==1) { ++i; Console.WriteLine (i); } } }
輸出
2
如果將Go方法的程式碼位置互換
static void Go() { if (i==1) { Console.WriteLine (i);++i;} }
輸出
1 1(有時輸出一個,有時輸出兩個)
如果新執行緒在Write之後,done=true之前,主執行緒也執行到了write那麼就會有兩個done。
不同執行緒在讀寫共享欄位時會出現不可控的輸出,這就是多執行緒的執行緒安全問題。
解決方法: 使用排它鎖來解決這個問題--lock
class ThreadSafe { static bool done; static readonly object locker = new object(); static void Main() { new Thread (Go).Start(); Go(); } static void Go() { //使用lock,確保一次只有一個執行緒執行該程式碼 lock (locker) { if (!done) { Console.WriteLine ("Done"); done = true; } } } }
當多個執行緒都在爭取這個排它鎖時,一個執行緒獲取該鎖,其他執行緒會處於blocked狀態(該狀態時不消耗cpu),等待另一個執行緒釋放鎖時,捕獲該鎖。這就保證了一次
只有一個執行緒執行該程式碼。
Join和Sleep
Join可以實現暫停另一個執行緒,直到呼叫Join方法的執行緒結束。
static void Main() { Thread t = new Thread (Go); t.Start(); t.Join(); Console.WriteLine ("Thread t has ended!"); } static void Go() { for (int i = 0; i < 1000; i++) Console.Write ("y"); }
輸出
yyyyyy..... Thread t has ended!
執行緒t呼叫Join方法,阻塞主執行緒,直到t執行緒執行結束,再執行主執行緒。
Sleep:暫停該執行緒一段時間
Thread.Sleep (TimeSpan.FromHours (1)); // 暫停一個小時 Thread.Sleep (500); // 暫停500毫秒 Join是暫停別的執行緒,Sleep是暫停自己執行緒。
上面的例子是使用Thread類的建構函式,給建構函式傳入一個ThreadStart委託。來實現的。
public delegate void ThreadStart();
然後呼叫Start方法,來執行該執行緒。委託執行完該執行緒也結束。
如:
class ThreadTest { static void Main() { Thread t = new Thread (new ThreadStart (Go)); t.Start(); // 執行Go方法 Go(); // 同時在主執行緒上執行Go方法 } static void Go() { Console.WriteLine ("hello!"); } }
多數情況下,可以不用new ThreadStart委託。直接在建構函式裡傳入void型別的方法。
Thread t = new Thread (Go);
使用lambda表示式
static void Main() { Thread t = new Thread ( () => Console.WriteLine ("Hello!") ); t.Start(); }
Foreground執行緒和Background執行緒
預設情況下建立的執行緒都是Foreground,只要有一個Foregournd執行緒在執行,應用程式就不會關閉。
Background執行緒則不是。一旦Foreground執行緒執行完,應用程式結束,background就會強制結束。
可以用IsBackground來檢視該執行緒是什麼型別的執行緒。
執行緒異常捕獲
public static void Main() { try { new Thread (Go).Start(); } catch (Exception ex) { // 不能捕獲異常 Console.WriteLine ("Exception!"); } } static void Go() { throw null; } //丟擲 Null異常
此時並不能在Main方法裡捕獲執行緒Go方法的異常,如果是Thread自身的異常可以捕獲。
正確捕獲方式:
public static void Main() { new Thread (Go).Start(); } static void Go() { try { // ... throw null; // 這個異常會被下面捕獲 // ... } catch (Exception ex) { // ... } }
執行緒池
當建立一個執行緒時,就會消耗幾百毫秒cpu,建立一些新的私有區域性變數棧。每個執行緒還消耗(預設)約1 MB的記憶體。執行緒池通過共享和回收執行緒,允許在不影響效能的情況下啟用多執行緒。
每個.NET程式都有一個執行緒池,執行緒池維護著一定數量的工作執行緒,這些執行緒等待著執行分配下來的任務。
執行緒池執行緒注意點:
1 執行緒池的執行緒不能設定名字(導致執行緒除錯困難)。
2 執行緒池的執行緒都是background執行緒
3 阻塞一個執行緒池的執行緒,會導致延遲。
4 可以隨意設定執行緒池的優先順序,在回到執行緒池時改執行緒就會被重置。
通過Thread.CurrentThread.IsThreadPoolThread.可以檢視該執行緒是否是執行緒池的執行緒。
使用執行緒池建立執行緒的方法:
- Task
- ThreadPool.QueueUserWorkItem
- Asynchronous delegates
- BackgroundWorker
TPL
Framework4.0下可以使用Task來建立執行緒池執行緒。呼叫Task.Factory.StartNew(),傳遞一個委託
- Task.Factory.StartNew
static void Main() { Task.Factory.StartNew (Go); } static void Go() { Console.WriteLine ("Hello from the thread pool!"); }
Task.Factory.StartNew 返回一個Task物件。可以呼叫該Task物件的Wait來等待該執行緒結束,呼叫Wait時會阻塞呼叫者的執行緒。
- Task建構函式 給Task建構函式傳遞Action委託,或對應的方法,呼叫start方法,啟動任務
static void Main() { Task t=new Task(Go); t.Start(); } static void Go() { Console.WriteLine ("Hello from the thread pool!"); }
static void Main() { Task.Run(() => Go()); } static void Go() { Console.WriteLine ("Hello from the thread pool!"); }
QueueUserWorkItem
QueueUserWorkItem沒有返回值。使用 QueueUserWorkItem,只需傳遞相應委託的方法就行。
static void Main() { //Go方法的引數data此時為空 ThreadPool.QueueUserWorkItem (Go); //Go方法的引數data此時為123 ThreadPool.QueueUserWorkItem (Go,123); Console.ReadLine(); } static void Go (object data) { Console.WriteLine ("Hello from the thread pool! " + data); }
委託非同步
委託非同步可以返回任意型別個數的值。
使用委託非同步的方式:
- 宣告一個和方法匹配的委託
- 呼叫該委託的BeginInvoke方法,獲取返回型別為IAsyncResult的值
- 呼叫EndInvoke方法傳遞IAsyncResulte型別的值獲取最終結果
如:
static void Main() { Func<string,int> method = Work; IAsyncResult cookie = method.BeginInvoke ("test",null,null); // // ... 此時可以同步處理其他事情 // int result = method.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); } static int Work (string s) { return s.Length; }
使用回撥函式來簡化委託的非同步呼叫,回撥函式引數為IAsyncResult型別
static void Main() { Func<string,int> method = Work; method.BeginInvoke ("test",Done,method); // ... //並行其他事情 } static int Work (string s) { return s.Length; } static void Done (IAsyncResult cookie) { var target = (Func<string,int>) cookie.AsyncState; int result = target.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); }
使用匿名方法
Func<string,int> f = s => { return s.Length; }; f.BeginInvoke("hello",arg => { var target = (Func<string,int>)arg.AsyncState; int result = target.EndInvoke(arg); Console.WriteLine("String length is: " + result); },f);
執行緒傳參和執行緒返回值
Thread
Thread建構函式傳遞方法有兩種方式:
public delegate void ThreadStart(); public delegate void ParameterizedThreadStart (object obj);
所以Thread可以傳遞零個或一個引數,但是沒有返回值。
- 使用lambda表示式直接傳入引數。
static void Main() { Thread t = new Thread ( () => Print ("Hello from t!") ); t.Start(); } static void Print (string message) { Console.WriteLine (message); }
static void Main() { Thread t = new Thread (Print); t.Start ("Hello from t!"); } static void Print (object messageObj) { string message = (string) messageObj; Console.WriteLine (message); }
Lambda簡潔高效,但是在捕獲變數的時候要注意,捕獲的變數是否共享。
如:
for (int i = 0; i < 10; i++) new Thread (() => Console.Write (i)).Start();
輸出
0223447899
因為每次迴圈中的i都是同一個i,是共享變數,在輸出的過程中,i的值會發生變化。
解決方法-區域性域變數
for (int i = 0; i < 10; i++) { int temp = i; new Thread (() => Console.Write (temp)).Start(); }
這時每個執行緒都指向新的域變數temp(此時每個執行緒都有屬於自己的花括號的域變數)在該執行緒中temp不受其他執行緒影響。
委託
委託可以有任意個傳入和輸出引數。以Action,Func來舉例。
- Action 有零個或多個傳入引數,但是沒有返回值。
- Func 有零個或多個傳入引數,和一個返回值。
Func<string,int> method = Work; IAsyncResult cookie = method.BeginInvoke("test",null); // // ... 此時可以同步處理其他事情 // int result = method.EndInvoke(cookie); Console.WriteLine("String length is: " + result); int Work(string s) { return s.Length; }
使用回撥函式獲取返回值
static void Main() { Func<string,null); // ... //並行其他事情 } static int Work (string s) { return s.Length; } static void Done (IAsyncResult cookie) { var target = (Func<string,int>) cookie.AsyncState; int result = target.EndInvoke (cookie); Console.WriteLine ("String length is: " + result); }
EndInvoke做了三件事情:
- 等待委託非同步的結束。
- 獲取返回值。
- 丟擲未處理異常給呼叫執行緒。
Task
Task泛型允許有返回值。
如:
static void Main() { // 建立Task並執行 Task<string> task = Task.Factory.StartNew<string> ( () => DownloadString ("http://www.baidu.com") ); // 同時執行其他方法 Console.WriteLine("begin"); //等待獲取返回值,並且不會阻塞主執行緒 Console.WriteLine(task.Result); Console.WriteLine("end"); } static string DownloadString (string uri) { using (var wc = new System.Net.WebClient()) return wc.DownloadString (uri); }
參考:
http://www.albahari.com/threading/
以上就是C# 執行緒相關知識總結的詳細內容,更多關於C# 執行緒的資料請關注我們其它相關文章!