線程概述 線程是一個獨立處理的執行路徑。每個線程都運行在一個操作系統進程中,這個進程是程式執行的獨立環境。在單線程中進程的獨立環境內只有一個線程運行,所以該線程具有獨立使用進程資源的權利。在多線程程式中,在進程中有多個線程運行,所以它們共用同一個執行環境。 基礎線程(thread) 使用Thread ...
線程概述
線程是一個獨立處理的執行路徑。每個線程都運行在一個操作系統進程中,這個進程是程式執行的獨立環境。在單線程中進程的獨立環境內只有一個線程運行,所以該線程具有獨立使用進程資源的權利。在多線程程式中,在進程中有多個線程運行,所以它們共用同一個執行環境。
基礎線程(thread)
使用Thread類可以創建和控制線程,定義在System.Threading命名空間中:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int mainId = Thread.CurrentThread.ManagedThreadId; 6 Console.WriteLine("主線程Id為:{0}", mainId); 7 //定義線程 8 Thread thread = new Thread(() => 9 { 10 Test("Demo-ok"); 11 }); 12 //啟動線程 13 thread.Start(); 14 Console.WriteLine("主線程Id為:{0}", mainId); 15 Console.ReadKey(); 16 } 17 static void Test(string o) 18 { 19 Console.WriteLine("工作者線程Id為:{0}", Thread.CurrentThread.ManagedThreadId); 20 Console.WriteLine("執行方法:{0}", o); 21 } 22 /* 23 * 作者:Jonins 24 * 出處:http://www.cnblogs.com/jonins/ 25 */ 26 }
執行結果(執行結果並不固定):
主線程創建一個新線程thread在上面運行一個方法Test。同時主線程也會繼續執行。在單核電腦上,操作系統會給每一個線程分配一些"時間片"(winodws一般為20毫秒),用於模擬併發性。而在多核/多處理器主機上線程卻能夠真正實現並行執行(分別由電腦上其它激活處理器完成)。
線程常用方法
Thread在.NET Framework 1.1起引入是最早的多線程處理方式,他包含了幾種最常用的方法如下,
Start | 開啟線程(停止後的線程無法再次啟用) |
Suspend | 暫停(掛起)線程(已過時,不推薦使用) |
Resume | 恢復暫停(掛起)的線程(已過時,不推薦使用) |
Intterupt | 中斷線程 |
Abort | 銷毀線程 |
IsAlive | 獲取當前線程的執行狀態(True-運行,False-停止) |
Join |
方法是非靜態方法,使得在系統調用此方法時只有這個線程執行完後,才能執行其他線程,包括主線程的終止! 或者給它制定時間,即最多過了這麼多時間後,如果還是沒有執行完,下麵的線程可以繼續執行而不必再理會當前線程是否執行完。 |
Thread.Sleep |
方式是Thread類靜態方法,在調用出使得該線程暫停一段時間 |
註意:
不要使用Suspend和Resume方法來同步線程的活動。當你Suspend線程時,您無法知道線程正在執行什麼代碼。如果在安全許可權評估期間線程持有鎖時掛起線程,則AppDomain中的其他線程可能會被阻塞。如果線程在執行類構造函數時Suspend,則試圖使用該類的AppDomain中的其他線程將被阻塞。死鎖很容易發生。
後臺/前臺線程 &阻塞
前臺進程和後臺進程使用IsBackground屬性設置。此狀態與線程的優先順序(執行時間分配)無關。
前臺進程:Thread預設為前臺線程,程式關閉後,線程仍然繼續,直到計算完為止。
後臺進程:將IsBackground屬性設置為true,即為後臺進程,主線程關閉,所有子線程無論運行完否,都馬上關閉。
線程阻塞是指線程由於特定原因暫停執行,如Sleeping或執行Join後等待另一個線程停止。阻塞的線程會立刻交出”時間片“, 並從此時開始不再消耗處理器的時間,直至阻塞條件結束。使用線程的ThreadState屬性,可以測試線程的阻塞狀態。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Thread thread = new Thread(() => 6 { 7 Test("Demo-ok"); 8 }); 9 var state = thread.ThreadState; 10 Console.WriteLine("子線程開啟前ThreadState:{0}", state); 11 //開啟線程 12 thread.Start(); 13 state = thread.ThreadState; 14 Console.WriteLine("子線程開啟後ThreadState:{0}", state); 15 //阻塞主線程1秒 16 Thread.Sleep(1000); 17 state = thread.ThreadState; 18 Console.WriteLine("子線程阻塞時ThreadState:{0}", state); 19 //主線程等待子線程執行完成 20 thread.Join(); 21 state = thread.ThreadState; 22 Console.WriteLine("子線程執行完成ThreadState:{0}", state); 23 Console.ReadKey(); 24 } 25 static void Test(string o) 26 { 27 //阻塞子線程2秒 28 Thread.Sleep(2000); 29 Console.WriteLine("方法執行完成!返回值:{0}", o); 30 } 31 /* 32 * 作者:Jonins 33 * 出處:http://www.cnblogs.com/jonins/ 34 */ 35 }
結果如下:
ThreadState是一個標記枚舉量,我們只大約常用的記住這四個狀態即可,其它因為API中棄用了一部分如掛起等不必考慮:
Running | 啟動線程 |
Stopped | 該線程已停止 |
Unstarted | 未開啟 |
WaitSleepJoin | 線程受阻 |
註意:
1.當線程阻塞時,操作系統執行環境(線程上下文)切換,會增加負載,幅度一般在1-2毫秒左右。
2.ThreadState屬性只是用於調試程式,絕對不要用ThreadState來同步線程活動,因為線程狀態可能在測試ThreadState和獲取這個信息的時間段內發生變化。
線程優先順序
當多個線程同時運行時,可以對同時運行的多個線程設置優先順序,優先處理級別高的線程(一般情況下,如果有優先順序較高的線程在工作,就不會給優先順序較低的線程分配任何時間片)。1 xxx.Priority = ThreadPriority.Normal;
線程優先順序通過Priority屬性設置,Priority屬性是一個ThreadPriority枚舉
AboveNormal | 高於正常 |
BelowNormal | 低於正常 |
Highest | 最高 |
Lowest | 最低 |
Normal | 正常 |
ThreadStart&ParameterizedThreadStart
Thread重載的其它四種構造函數需要帶入特殊對象,分別是ThreadStart和ParameterizedThreadStart類。
ThreadStart類本質是一個無參數無返回值的委托。
1 public delegate void ThreadStart();
ParameterizedThreadStart類本質是有一個object類型參數無返回值的委托。
1 public delegate void ParameterizedThreadStart(object obj);
使用方式如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 int mainId = Thread.CurrentThread.ManagedThreadId; 6 Console.WriteLine("主線程Id為:{0}", mainId); 7 //ThreadStart構造函數創建線程 8 { 9 ThreadStart threadStart = new ThreadStart(TestOne); 10 Thread threadOne = new Thread(threadStart); 11 threadOne.Start(); 12 } 13 //ParameterizedThreadStart構造函數創建線程 14 { 15 ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart(TestTwo); 16 Thread threadTwo = new Thread(parameterizedThreadStart); 17 threadTwo.Start("DemoTwo-ok"); 18 } 19 Console.WriteLine("主線程Id為:{0}", mainId); 20 Console.ReadKey(); 21 } 22 private static void TestOne() 23 { 24 Console.WriteLine("執行方法:DemoOne-ok,工作者線程Id為:{0}", Thread.CurrentThread.ManagedThreadId); 25 } 26 private static void TestTwo(object o) 27 { 28 Console.WriteLine("執行方法:{0},工作者線程Id為:{1}", o, Thread.CurrentThread.ManagedThreadId); 29 } 30 /* 31 * 作者:Jonins 32 * 出處:http://www.cnblogs.com/jonins/ 33 */ 34 }
執行結果(執行結果不固定):
因為ThreadStart和ParameterizedThreadStart為委托,所以我們也可以把符合要求的自定義委托或者內置委托進行轉換帶入構造函數。例如:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Action action = Test; 6 Thread thread = new Thread(new ThreadStart(action)); 7 thread.Start(); 8 Console.ReadKey(); 9 } 10 private static void Test() 11 { 12 Console.WriteLine("執行方法:Demo-ok"); 13 } 14 }
註意:
在需要傳遞參數時ParameterizedThreadStart構造線程和使用lambda表達式構建線程有著極大的區別:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //DemoOne();//數據被主線程修改 6 //DemoTwo(); 7 Console.ReadKey(); 8 } 9 static void DemoOne() 10 { 11 string message = "XXXXXX"; 12 Thread thread = new Thread(() => Test(message)); 13 thread.Start(); 14 message = "YYYYYY"; 15 } 16 static void DemoTwo() 17 { 18 19 string message = "XXXXXX"; 20 ParameterizedThreadStart parameterizedThreadStart = Test; 21 Thread thread = new Thread(parameterizedThreadStart); 22 thread.Start(message); 23 message = "YYYYYY"; 24 } 25 private static void Test(object o) 26 { 27 for (int i = 0; i < 1000; i++) 28 { 29 Console.WriteLine( o); 30 } 31 } 32 /* 33 * 作者:Jonins 34 * 出處:http://www.cnblogs.com/jonins/ 35 */ 36 }
上述案例對比DemoOne和DemoTwo的執行結果我們可以得到:
1.使用lamdba表達式構建線程時,變數由引用捕獲,父線程中的任何更改都將影響子線程內的值。且lamda是在實際執行時捕獲變數而不是線上程開始時捕獲變數,如果在父線程中修改參數值子線程內的值也會受到影響。
2.而ParameterizedThreadStart則是線上程啟動是捕獲變數,啟動後父線程修改變數值子線程內的值不會受到影響。
本地/共用狀態
CLR會給每一個線程分配獨立的記憶體堆,從而保證本地變數的隔離。而多個線程訪問相同的對象,並對共用狀態的訪問沒有同步,此時就會出現數據爭用的問題從而引發程式間歇性錯誤,這也是多線程經常被詬病的緣由。
局部(本地)變數每個線程的記憶體堆都會創建變數副本。
如果線程擁有同一個對象實例的通用引用,那麼這些線程就會共用數據。
1 public class ThreadInstance 2 { 3 //共用變數 4 bool flag; 5 public void Demo() 6 { 7 new Thread(Test).Start();//子線程執行一次方法 8 Thread.Sleep(1000); 9 Test();//主線程執行一次方法 10 Console.ReadKey(); 11 } 12 void Test() 13 { 14 //線程內局部變數 15 bool localFlag=true; 16 Console.WriteLine("localFlag:{0}", localFlag); 17 localFlag = !localFlag; 18 if (!flag) 19 { 20 Console.WriteLine("flag:{0}", flag); 21 flag = !flag; 22 } 23 } 24 }
執行Demo方法結果:
因為兩個線程都在同一個ThreadInstance實例上調用方法,所以它們共用flag,因此flag變數只會列印一次。而localFlag為局部變數所以兩個線程內變數相互不影響。
註意:
1.編譯器會將lambda表達式或匿名代理捕獲的局部變數轉換為域,它們會共用數據。
2.靜態域線程之間也會共用數據。
線程同步
在多個線程同時對同一個記憶體地址進行寫入,由於CPU時間調度上的問題,寫入數據會被多次的覆蓋,所以就要使線程同步。
線程同步:一個線程在對記憶體進行操作時,其他線程都不可以對這個記憶體地址進行操作,直到該線程完成操作, 其他線程才能對該記憶體地址進行操作。
同步結構可以分三大類:
排他鎖:排他鎖結構只允許一個線程執行特定的活動,它們的主要目標是允許線程訪問共用的寫狀態,但不會互相影響。包括(lock、Mutex、SpinLock)。
非排他鎖:非排他鎖只能實現有限的併發性。包括(Semaphore、ReaderWriterLock)。
發送信號:允許線程保持阻塞,直到從其它線程接受到通知。包括(ManualResetEvent、AutoResetEvent、CountdownEvent和Barrier)
排他鎖 lock&Mutex&SpinLock
1.內核鎖 Lock&Monitor
Lock:保證當多個線程同時爭奪同一個鎖時,每次只有一個線程可以鎖定同步對象,其他線程會等待(或阻塞)在加鎖位置,直到鎖釋放,其它線程才可以繼續訪問。如果多個線程爭奪同一個鎖,那麼它們會在一個準備隊列中排隊,以先到先得的方式分配鎖。排他鎖有時候也稱為對鎖保護的對象添加序列化訪問許可權,因為一個線程的訪問不會與其他線程的訪問重疊。
lock使用的示例如下,Demo未加鎖,DemoTwo加鎖:
1 public class ThreadInstance 2 { 3 //--------------Demo---------------- 4 public void Demo() 5 { 6 new Thread(Test).Start(); 7 Test(); 8 } 9 private bool Flag { get; set; } 10 void Test() 11 { 12 Console.WriteLine("Demo-Flag:{0}", Flag); 13 Thread.Sleep(1000);//阻塞子線程,讓主線程運行下來 14 Flag = true; 15 } 16 //--------------DemoTwo---------------- 17 public void DemoTwo() 18 { 19 new Thread(TestTow).Start(); 20 TestTow(); 21 } 22 private bool FlagTow { get; set; } 23 readonly object Locker = new object(); 24 void TestTow() 25 { 26 //加鎖,阻塞主線程直至子線程執行完畢 27 lock (Locker) 28 { 29 Console.WriteLine("TestTow-FlagTow:{0}", FlagTow); 30 Thread.Sleep(1000);//阻塞子線程,讓主線程運行下來 31 FlagTow = true; 32 } 33 } 34 /* 35 * 作者:Jonins 36 * 出處:http://www.cnblogs.com/jonins/ 37 */ 38 }
執行結果如下:
Demo:不具有線程安全性,兩個線程同時調用Test,會出現兩次False,因為主線程執行時子線程變數還沒有改變。
DemoTwo:保證每次只有一個線程可以鎖定同步對象(Locker),其他競爭線程(本例即主線程)都會阻塞在這個位置,直至鎖釋放,所以會列印一次False和一次True。
lock語句是Monitor.Enter和Monitor.Exit方法調用try/finally語句塊的簡寫語法。
1 lock (Locker) 2 { 3 ... 4 } 5 //-------兩者等價------- 6 Monitor.Enter(Locker); 7 try 8 { 9 ... 10 } 11 finally 12 { 13 Monitor.Exit(Locker); 14 }
但此寫法在方法調用和語句塊之間若拋出異常,鎖將無法釋放,因為執行過程無法再進入try/finally語句塊,導致鎖泄露,優化方法是使用Monitor.Enter重載,同時可以使用Monitor.TryEnter方法指定一個超時時間。
1 bool lockTaken = false; 2 Monitor.Enter(Locker, ref lockTaken); 3 try 4 { 5 ... 6 } 7 finally 8 { 9 if (lockTaken) 10 Monitor.Exit(Locker); 11 }
2.互斥鎖 Mutex
Mutex:類似於C#的Lock,但是它可以支持多個進程。所以Mutex可用於電腦範圍或應用範圍。使用Mutex類,就可以調用WaitOne方法獲得鎖,ReleaseMutex釋放鎖,關閉或去掉一個Mutex會自動釋放互斥鎖。
示例來自https://msdn.microsoft.com/zh-cn/library/system.threading.mutex(v=vs.110).aspx ,如需更詳細請訪問MSDN。
1 class Program 2 { 3 //創建一個新的互斥。創建線程不擁有互斥對象。 4 private static Mutex mut = new Mutex(); 5 private const int numThreads = 3; 6 static void Main(string[] args) 7 { 8 //創建將使用受保護資源的線程 9 for (int i = 0; i < numThreads; i++) 10 { 11 Thread newThread = new Thread(new ThreadStart(ThreadProc)); 12 newThread.Name = String.Format("Thread{0} :", i + 1); 13 newThread.Start(); 14 } 15 Console.ReadKey(); 16 } 17 private static void ThreadProc() 18 { 19 Console.WriteLine("{0}請求互斥鎖", Thread.CurrentThread.Name); 20 // 等待,直到安全進入,如果請求超時,不會獲得互斥量 21 if (mut.WaitOne(3000)) 22 { 23 Console.WriteLine("{0}進入保護區了", Thread.CurrentThread.Name); 24 { 25 //模擬一些工作 26 Thread.Sleep(2000); 27 Console.WriteLine("{0}執行了工作 ", Thread.CurrentThread.Name); 28 } 29 // 釋放互斥鎖。 30 mut.ReleaseMutex(); 31 Console.WriteLine("{0}釋放了互斥鎖 ", Thread.CurrentThread.Name); 32 } 33 else 34 { 35 Console.WriteLine("{0}不會獲得互斥量", Thread.CurrentThread.Name); 36 } 37 } 38 }
註意:
1.給Mutex命名,使之整個電腦範圍有效,這個名稱應該在公司和應用程式中保持唯一。
2.獲得和釋放一個無爭奪的Mutex需要幾毫秒,時間比lock操作慢50倍。
3.自旋鎖 SpinLock
SpinLock 在.NET 4.0引入,內部實現了微優化,可以減少高度併發場景的上下文切換。示例如下:
1 class ThreadInstance 2 { 3 public void Demo() 4 { 5 Thread thread = new Thread(() => Test()); 6 thread.Start(); 7 Test(); 8 Console.ReadKey(); 9 } 10 SpinLock spinLock = new SpinLock(); 11 bool Flag; 12 void Test() 13 { 14 bool gotLock = false; //釋放成功 15 //進入鎖 16 spinLock.Enter(ref gotLock); 17 { 18 Console.WriteLine(Flag); 19 Flag = !Flag; 20 } 21 if (gotLock) spinLock.Exit();//釋放鎖 22 } 23 }
執行結果如下,若註釋掉代碼行spinLock.Enter(ref gotLock);這段程式就會出現問題會列印兩次False:
排他鎖總結:
lock(內核鎖) | |
本質 | 基於內核對象構造的鎖機制,它發現資源被鎖住時,請求進入排隊等待,直到鎖釋放再繼續訪問資源 |
優點 | CPU利用最大化。 |
缺點 | 線程上下文切換損耗性能。 |
Mutex(互斥鎖) | |
本質 | 多線程共用資源時,當一個線程占用Mutex對象時,其它需要占用Mutex的線程將處於掛起狀態,直到Mutex被釋放。 |
優點 |
可以跨應用程式邊界對資源進行獨占訪問,即可以用同步不同進程中的線程。 |
缺點 | 犧牲更多的系統資源。 |
SpinLock(自旋鎖) | |
本質 | 不會讓線程休眠,而是一直迴圈嘗試對資源的訪問,直到鎖釋放資源得到訪問。 |
優點 | 被阻塞時,不進行上下文切換,而是空轉等待。對多核CPU而言,減少了切換線程上下文的開銷。 |
缺點 | 長時間的迴圈導致CPU的浪費,高併發競爭下,CPU的損耗嚴重。 |
非排他鎖 SemaphoreSlim&ReaderWriterLockSlim
1.信號量 SemaphoreSlim
信號量(SemaphoreSlim)類似於一個閥門,只允許特定容量的線程進入,超出容量的線程則不允許再進入只能在後面排隊(先到先進)。容量為1的信號量與Mutex或lock相似,但是信號量與線程無關,任何線程都可以釋放,而Mutex和lock,只有獲得鎖的線程才可以釋放。
下麵示例5個線程同時請求但只有3個線程可以同時訪問:
1 class Program 2 { 3 /// <summary> 4 /// 聲明信號量,容量3 5 /// </summary> 6 static SemaphoreSlim semaphoreSlim = new SemaphoreSlim(3); 7 static void Main(string[] args) 8 { 9 for (int i = 0; i < 5; i++) 10 { 11 new Thread(Enter).Start(i); 12 } 13 Console.ReadKey(); 14 } 15 static void Enter(object id) 16 { 17 Console.WriteLine("準備訪問:{0}", id); 18 semaphoreSlim.Wait(); 19 //只有3個線程可以同時訪問 20 { 21 Console.WriteLine("開始訪問:{0}", id); 22 Thread.Sleep(1000 * (int)id); 23 Console.WriteLine("已經離開:{0}", id); 24 } 25 semaphoreSlim.Release(); 26 } 27 }
信號量可限制併發處理,防止太多線程同時執行特定代碼。這個類有兩個功能相似的版本:Semaphore和SemaphoreSlim。後者是.NET 4.0引入的,進行了一些優化,以滿足並行編程的低延遲要求。SemaphoreSlim適用於傳統多線程編程,因為它可以再等待時指定一個取消令牌。然而它並不適用於進程間通信。Semaphore在調用WaitOne或Release時需要消耗約1毫秒時間,而SemaphoreSlim的延遲時間只有前者1/4。
2.讀/寫鎖 ReaderWriterLockSlim
一些資源訪問,當讀操作很多而寫操作很少時,限制併發訪問並不合理,這種情況可能發生在業務應用伺服器,它會將常用的數據緩存在靜態域中,用以加塊訪問速度。使用ReaderWriterLockSlim類,可以在這種情況中實現鎖的最大可用性。
ReaderWriterLockSlim在.NET 3.5引入,目的是替換ReaderWriterLock類。兩者功能相似,但後者執行速度要慢好幾倍,且本身存在一些鎖升級處理的設計缺陷。與常規鎖(lock)相比,ReaderWriterLockSlim執行速度仍然要慢一倍。
下麵示例,有3個線程不停的獲取鏈表內元素總個數,同時有2個線程每個1秒鐘向鏈表添加隨機數:
1 class Program