C#執行緒入門
前言,多執行緒在日常編碼中經常會用,本文就主執行緒和子執行緒之間的關係做個大概的總結,若有差錯,歡迎斧正。
首先開啟工作管理員,檢視當前執行的程序:選單欄右鍵選擇“Task Manager(任務管理)”或 “Ctrl”+“Alt”+“Delete”點選“Task Manager(任務管理)”
從工作管理員裡面可以看到當前所有正在執行的程序。
Tab點選“效能”:
可以看到當前正在執行的所有程序數“Processes”和執行緒數“Threads”
一般來說一個應用程式就對應一個程序,一個程序可有一個或多個執行緒即(>=1個執行緒),有且只有一個主執行緒。
程序和執行緒:
程序:是Windows系統中的一個基本概念,它包含著一個執行程式所需要的資源。
執行緒:是程序中的基本執行單元,是作業系統分配CPU時間的基本單位,一個程序可以包含若干個執行緒,在程序入口執行的第一個執行緒被視為這個程序的主執行緒。在.NET應用程式中,都是以Main()方法作為入口的,當呼叫此方法時系統就會自動建立一個主執行緒。執行緒主要是由CPU暫存器、呼叫棧和執行緒本地儲存器(Thread Local Storage,TLS)組成的。CPU暫存器主要記錄當前所執行執行緒的狀態,呼叫棧主要用於維護執行緒所呼叫到的記憶體與資料,TLS主要用於存放執行緒的狀態資訊。
前臺執行緒後後臺執行緒:
前臺執行緒:預設情況下建立的執行緒都是前臺執行緒,只有所有的前臺執行緒關閉才能完成程式關閉。只有將執行緒屬性IsBackground設定為true,才是後臺執行緒
後臺執行緒:程式關閉,後臺執行緒不管是否執行完都將關閉
示例如下:
static void Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; //前臺執行緒 Thread qtxc = new Thread(() => RunLoop(10)); qtxc.Name = "qiantai thread"; //後臺執行緒 Thread htxc = new Thread(() => RunLoop(20)); htxc.Name = "houtai thread"; htxc.IsBackground = true; //將執行緒設定為後臺執行緒 //啟動執行緒 qtxc.Start(); htxc.Start(); } public static void RunLoop(int count) { //獲取當前執行緒名稱 string threadName = Thread.CurrentThread.Name; for (int i = 0; i < count; i++) { Console.WriteLine($"{threadName} index:{(i + 1).ToString()}"); //休眠1秒 Thread.Sleep(1000); } Console.WriteLine($"{threadName} complete"); }
程式碼中定義了一個迴圈列印的方法,其中前臺執行緒列印10次,後臺執行緒列印20次,執行結果如下:
從執行結果可以看出,前臺執行緒列印完成後,後臺執行緒未列印完,但是程式自動結束了。
然後調整引數,前臺執行緒列印20次,後臺執行緒列印10次:
class Program { static void Main(string[] args) { //前臺執行緒 Thread qtxc = new Thread(() => RunLoop(20)); qtxc.Name = "qiantai thread"; //後臺執行緒 Thread htxc = new Thread(() => RunLoop(10)); htxc.Name = "houtai thread"; htxc.IsBackground = true; //將執行緒設定為後臺執行緒 //啟動執行緒 qtxc.Start(); htxc.Start(); } public static void RunLoop(int count) { //獲取當前執行緒名稱 string threadName = Thread.CurrentThread.Name; for (int i = 0; i < count; i++) { Console.WriteLine($"{threadName} index:{(i + 1).ToString()}"); //休眠1秒 Thread.Sleep(1000); } Console.WriteLine($"{threadName} complete"); } }
執行結果如下:
從執行結果可以看出,後臺執行緒列印完成後,前臺執行緒未列印完繼續列印,前臺執行緒列印完成後,程式結束。
結論就是:前臺執行緒結束時,不管後臺執行緒是否結束,程式都將結束;後臺執行緒結束時,若有前臺執行緒再執行,程式將繼續執行,等到前臺執行緒執行完後,程式結束。
後臺執行緒一般用於處理不重要的事情,應用程式結束時,後臺執行緒是否執行完成對整個應用程式沒有影響。如果要執行的事情很重要,需要將執行緒設定為前臺執行緒。
主執行緒和子執行緒之間的關係:
- 預設情況,在新開啟一個子執行緒的時候,他是前臺執行緒,只有將執行緒的IsBackground屬性設為true;他才是後臺執行緒
- 當子執行緒是前臺執行緒,則主執行緒結束並不影響其他執行緒的執行,只有所有前臺執行緒都結束,程式結束
- 當子執行緒是後臺執行緒,則主執行緒的結束,會導致子執行緒的強迫結束
- 不管是前臺執行緒還是後臺執行緒,如果執行緒內出現了異常,都會導致程序的終止。
- 託管執行緒池中的執行緒都是後臺執行緒
C#中執行緒的使用:
引入名稱空間:System.Threading
Thread執行緒常用方法列表:
方法 | 說明 |
Start | 開始執行緒 |
Sleep | 使執行緒暫停指定的一段時間。如Thread.Sleep(3000),即執行緒暫停三秒後繼續執行 |
Abort | 線上程到達安全點時,使其停止。 |
Suspend | 線上程到達安全點時,使其暫停。 |
Resume | 重新啟動掛起的執行緒 |
安全點是指程式碼中公共語言執行時可以安全地執行自動“垃圾回收”的位置。 垃圾回收是指釋放不再使用的變數並回收記憶體的過程。 呼叫執行緒的 Abort 或 Suspend 方法時,公共語言執行時將對程式碼進行分析,確定讓執行緒停止執行的適當位置。 Thread常用屬性:
屬性 | 說明 |
IsAlive | 如果執行緒處於活動狀態,則返回TRUE |
IsBackground | 是否是後臺執行緒 |
Name | 獲取或設定執行緒的名稱。 通常用於在除錯時發現各個執行緒。 |
Priority | 獲取或設定作業系統用於確定執行緒排程優先順序的值 |
ApartmentState | 獲取或設定用於特定執行緒的執行緒模型。 執行緒模型線上程呼叫非託管程式碼時很重要。 |
ThreadState | 執行緒狀態 |
IsThreadPoolThread | 獲取一個值,該值指示執行緒是否屬於託管執行緒池。 |
ManagedThreadId | 獲取當前託管執行緒的唯一識別符號。 |
Normal
,但可以將 Priority 屬性更改為 ThreadPriority 列舉中的任何值
。
以上就是Thread類常用屬性即方法,其他用法可以檢視Thread底層。
執行緒同步:
所謂同步:實質就是在某一時刻,只有一個執行緒可以訪問變數;如果不能確保對變數的訪問是同步的,就會產生錯誤。
C#為同步訪問變數提供了一個非常簡單的方式,即使用“Lock”關鍵詞,它可以把一段程式碼定義為互斥段,互斥段在某一個時刻內,只允許一個執行緒進入執行,而其他執行緒必須等待。Lock使用語法如下:
Lock(expression){
statement_block
} 來個例項,就拿藥店賣藥為例子:假設藥店的“999感冒靈”總量是固定的,然後分發到各個藥店出售,一個藥店賣出一包,總量就會減少一包, 再不考慮執行緒同步的情況下, 示例如下:
class Program { static void Main(string[] args) { //開啟兩個執行緒來代表兩個分店 Shop shop = new Shop(); //執行緒1,分店1 Thread thread1 = new Thread(shop.Sell); thread1.Name = "shop1"; //執行緒2,分店2 Thread thread2 = new Thread(shop.Sell); thread2.Name = "shop2"; //開啟執行緒,開始賣藥 thread1.Start(); thread2.Start(); Console.ReadKey(); } } public class Shop { /// <summary> /// 剩餘“999感冒靈”的數量 /// </summary> public int num = 10; public void Sell() { do { string threadName = Thread.CurrentThread.Name; if (num > 0) { Thread.Sleep(1000); num -= 1; Console.WriteLine($"{threadName} sell a“999”,stock{num} pack"); } else { Console.WriteLine("stock nothing!"); } } while (num > 0);//判斷是否有庫存,如果有就可以持續出售 } }
執行結果:
從執行結果可以看出,線上程未同步的情況下,兩家藥店都在銷售庫存的藥,但是店鋪1銷售的時候不知道庫存沒有了,於是出現了庫存 “-1”的情況,顯然是有問題的;
然後調整方案,加入執行緒同步,示例如下:
class Program { static void Main(string[] args) { //開啟兩個執行緒來代表兩個分店 Shop shop = new Shop(); //執行緒1,分店1 Thread thread1 = new Thread(shop.Sell); thread1.Name = "shop1"; //執行緒2,分店2 Thread thread2 = new Thread(shop.Sell); thread2.Name = "shop2"; //開啟執行緒,開始賣藥 thread1.Start(); thread2.Start(); Console.ReadKey(); } } public class Shop { /// <summary> /// 剩餘“999感冒靈”的數量 /// </summary> public int num = 10; public void Sell() { do { string threadName = Thread.CurrentThread.Name; lock (this) //使用lock關鍵字解決執行緒同步的問題 { if (num > 0) { Thread.Sleep(1000); num -= 1; Console.WriteLine($"{threadName} sell a“999”,stock{num} pack"); } else { Console.WriteLine("stock nothing!"); } } } while (num > 0);//判斷是否有庫存,如果有就可以持續出售 } }
執行結果如下:
從執行結果可以看出,線上程同步的情況下,每個藥店都從庫存裡面拿藥,藥店1出售的時候,藥店2就等著,當藥店1銷售完最後一包藥,就沒有另外的銷售記錄了,無論是藥店1還是藥店2來拿藥,庫存都沒有了,這就是執行緒同步的重要性!
結論:expression代表你希望跟蹤的物件:
如果你想保護一個類的例項,一般地,你可以使用this;
如果你想保護一個靜態變數(如互斥程式碼段在一個靜態方法內部),一般使用類名就可以了
而statement_block就算互斥段的程式碼,這段程式碼在一個時刻內只可能被一個執行緒執行。
以上就是執行緒的入門級教程~
不積跬步,無以至千里;不積小流,無以成江海。ヾ(◍°∇°◍)ノ゙