線程概述
線程是一個獨立處理的執行路徑。每個線程都運行在一個操作系統進程中,這個進程是程序執行的獨立環境。在單線程中進程的獨立環境內只有一個線程運行,所以該線程具有獨立使用進程資源的權利。在多線程程序中,在進程中有多個線程運行,所以它們共享同一個執行環境。
基礎線程(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;
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 2 { 3 static ReaderWriterLockSlim readerWriter = new ReaderWriterLockSlim(); 4 static List<int> Items = new List<int>(); 5 static void Main(string[] args) 6 { 7 for (int i = 0; i < 3; i++) 8 { 9 new Thread(Read).Start(); 10 } 11 new Thread(Write).Start("A"); 12 new Thread(Write).Start("B"); 13 Console.ReadKey(); 14 } 15 static void Read() 16 { 17 while (true) 18 { 19 readerWriter.EnterReadLock();//進入讀取模式鎖定狀態 20 { 21 Console.WriteLine("Items總數:{0}", Items.Count); 22 Thread.Sleep(2000); 23 } 24 readerWriter.ExitReadLock();//推出讀取模式 25 } 26 } 27 static void Write(object id) 28 { 29 while (true) 30 { 31 int newNumber = GetRandNum(100); 32 readerWriter.EnterWriteLock();//進入寫入模式鎖定狀態 33 { 34 Items.Add(newNumber); 35 } 36 readerWriter.ExitWriteLock();//推出寫入模式 37 Console.WriteLine("線程:{0},已隨機數:{1}", id, newNumber); 38 Thread.Sleep(1000); 39 } 40 } 41 static Random random = new Random(); 42 static int GetRandNum(int max) 43 { 44 lock (random) 45 return random.Next(max); 46 } 47 }
ReaderWriterLockSlim類可以實現2種基本鎖(讀鎖和寫鎖)。寫鎖是全局排他鎖,讀鎖兼容其它的讀鎖。所以獲得寫鎖的線程會阻塞其它試圖獲得讀鎖或寫鎖的線程。但是如果沒有線程獲得寫鎖,那么任意數量的線程可以同時獲得讀鎖。
所有EnterXXX方法都有相應的TreXXX,它們可以接受Monitor.TryEnter風格的超時參數(如果資源爭奪嚴重,那么很容易出現超時情況),ReaderWriterLockSlim也提供了相應的方法為TryEnterReadLock,TryEnterWriteLock。
發送信號(ManualResetEvent、AutoResetEvent、CountdownEvent和Barrier)
發送信號包括ManualResetEvent(Slim)、AutoResetEvent、CountdownEvent和Barrier。
前三個就是所謂的事件等待處理器(event wait handles,於C#事件無關)。同時ManualResetEvent(Slim)和AutoResetEvent繼承自EventWaitHandle類,它們從基類繼承了所有的功能。
1.AutoResetEvent
AutoResetEvent就像驗票口,插入一張票據則只允許一人通過,當一個線程調用WaitOne會在驗票口等待或阻塞,調用Set方法則插入一張票據。如果多個線程調用WaitOne則會在驗票口進行排隊,票據可以來自於任意線程。
1 class Program 2 { 3 static AutoResetEvent autoReset = new AutoResetEvent(false);//聲明一個驗票口 4 static void Main(string[] args) 5 { 6 new Thread(Waiter).Start(); 7 Thread.Sleep(1000); 8 autoReset.Set();//生成票據 9 Console.ReadKey(); 10 } 11 static void Waiter() 12 { 13 Console.WriteLine("等待");//線程在此等待,直到票據產生 14 autoReset.WaitOne(); 15 Console.WriteLine("通知"); 16 } 17 }
2.ManualResetEvent
ManualResetEvent的作用像是一扇大門,調用Set可以打開大門,使任意線程可以調用WaitOne,然后獲得允許進入大門的權限。調用Reset,則可以關閉大門。在已經關閉的大門上調用WaitOne的線程會進入阻塞狀態,當大門再次打開時這些線程會釋放。
1 class Program 2 { 3 4 static ManualResetEvent manualReset = new ManualResetEvent(false);//聲明一個閘門 5 static void Main(string[] args) 6 { 7 new Thread(Waiter).Start(); 8 new Thread(Waiter).Start(); 9 Thread.Sleep(2000); 10 manualReset.Set();//打開門 11 manualReset.Reset();//關閉門 12 new Thread(Waiter).Start(); 13 Thread.Sleep(2000); 14 manualReset.Set();//打開門 15 Console.ReadKey(); 16 } 17 static void Waiter() 18 { 19 Console.WriteLine("等待");//線程在此等待,直到大門打開 20 manualReset.WaitOne(); 21 Console.WriteLine("通知"); 22 } 23 }
3.CountdownEvent
CountdownEvent允許等待多個線程。它的作用像是計數器,計數器設置一個計數總量,多個線程調用Signal的次數達到計數總量時,調用WaitOne的線程將被釋放(不依賴於操作系統且優化了自旋結構,速度要比前兩者快50倍)。
1 class Program 2 { 3 static CountdownEvent countdownEvent = new CountdownEvent(3);//聲明一個計數器,總量3 4 static void Main(string[] args) 5 { 6 new Thread(Demo).Start("A"); 7 new Thread(Demo).Start("B"); 8 new Thread(Demo).Start("C"); 9 countdownEvent.Wait();//阻塞,直至Signal調用了3次 10 Console.WriteLine("所有子線程都經過了登記"); 11 Console.ReadKey(); 12 } 13 static void Demo(object o) 14 { 15 Console.WriteLine("線程:{0},已登記", o); 16 Thread.Sleep(2000); 17 countdownEvent.Signal(); 18 } 19 }
4.Barriet
Barriet類可以實現一個線程執行屏障,允許多個線程在同一時刻會合(如下圖所示),這個類執行速度很快非常高效,基於Wait,Pulse和自旋鎖實現。
Barriet類使用步驟:
1.創建它的實例,指定參與會合的線程數量,通過調用AddParticipants和RemoveParticipants修改此值。
2.當需要會合時,在每個線程上調用SignalAndWait。
1 class Program 2 { 3 static Barrier barrier = new Barrier(3);//初始化為3 4 static void Main(string[] args) 5 { 6 new Thread(Speak).Start(); 7 new Thread(Speak).Start(); 8 new Thread(Speak).Start(); 9 Console.ReadKey(); 10 } 11 static void Speak() 12 { 13 for (int i = 0; i < 5; i++) 14 { 15 Console.Write(i + " "); 16 //進入阻塞狀態,當調用3次后,”會合統一“執行,然后重新開始計數,這樣可以讓各個線程步調一致執行。 17 barrier.SignalAndWait(); 18 } 19 } 20 }
線程本地存儲
上面主要是解決線程並發訪問數據的問題。但有時候也需要保持數據隔離,以保證每個線程都擁有自己的副本。本地變量就可以實現這個目標,但是它們只適用於保存臨時數據。解決方案是使用線程本地存儲。
線程本地存儲有三種方式:ThreadStatic、ThreadLocal<T>和LocalDataStoreSlot(線程槽)
1.ThreadStatic
實現線程本地存儲的最簡單的方法時使用ThreadStatic靜態修飾符,是每個線程都可以使用獨立的變量副本,但是ThreadStatic不適用於實力域,也不適用於域的對象初始化。它們只能在調用靜態高走方法的線程上執行一次。如果需要處理實例域,那么更適合適用ThreadLocal<T>
1 class Program 2 { 3 [ThreadStatic] 4 private static string code = "string"; 5 static void Main(string[] args) 6 { 7 //在主線程設置只能被主線程讀取,其它線程無法訪問 8 //若在子線程中設置,則只有子線程可以訪問,其他線程無法訪問 9 Thread thread = new Thread(() => 10 { 11 code = "object"; 12 Console.WriteLine("子線程中讀取數據:{0}", code); 13 }); 14 thread.Start(); 15 Console.WriteLine("主線程中讀取數據:{0}", code); 16 Console.ReadKey(); 17 } 18 }
2.ThreadLocal<T>
ThreadLocal<T>支持創建靜態域和實例域的線程本地存儲,並且允許默認值。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 ThreadLocal<string> threadLocal = new ThreadLocal<string>(()=>"string"); 6 //在主線程設置只能被主線程讀取,其它線程無法訪問 7 //若在子線程中設置,則只有子線程可以訪問,其他線程無法訪問 8 //threadLocal.Value = "object"; 9 Thread thread = new Thread(() => 10 { 11 threadLocal.Value = "object"; 12 Console.WriteLine("子線程中讀取數據:{0}", threadLocal.Value); 13 }); 14 15 thread.Start(); 16 //主線程中讀取數據 17 Console.WriteLine("主線程中讀取數據:{0}", threadLocal.Value); 18 Console.ReadKey(); 19 } 20 }
3.LocalDataStoreSlot
使用Thred類的兩個方法GetData和SetData。這兩個方法會將數據存儲在線程獨有的“插槽”中。需要使用LocalDataStoreSlot對象來獲得這個存儲插槽。所有線程都可以使用相同的插槽。而創建一個命名插槽,整個應用程序將共享這個插槽。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //在主線程封裝內存槽,2種情況 6 //1.命名槽位 7 LocalDataStoreSlot localDataStoreSlot = Thread.AllocateNamedDataSlot("demo"); 8 //2.未命名槽位 9 //LocalDataStoreSlot localDataStoreSlot = Thread.AllocateDataSlot(); 10 //在主線程設置槽位,使此objcet類型數據智能被主線程讀取,其它線程無法訪問 11 //若在子線程中設置,則只有子線程可以訪問,其他線程無法訪問 12 Thread.SetData(localDataStoreSlot, "object"); 13 Thread thread = new Thread(() => 14 { 15 Console.WriteLine("子線程中讀取數據:{0}", Thread.GetData(localDataStoreSlot)); 16 }); 17 Console.WriteLine("主線程中讀取數據:{0}", Thread.GetData(localDataStoreSlot)); 18 thread.Start(); 19 Console.ReadKey(); 20 } 21 }
線程回調模擬
線程模擬回調函數的方式如下:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Action callback = () => 6 { 7 Console.WriteLine("回調方法-ok"); 8 }; 9 ThreadBeginInvoke(Test, callback); 10 Console.ReadKey(); 11 } 12 static void Test() 13 { 14 Console.WriteLine("執行方法-ok"); 15 } 16 static void ThreadBeginInvoke(ThreadStart method, Action callback) 17 { 18 ThreadStart threadStart = new ThreadStart(() => 19 { 20 method.Invoke(); 21 callback.Invoke(); 22 }); 23 Thread thread = new Thread(threadStart); 24 thread.Start(); 25 } 26 }
大致的思路如此,根據所需自行封裝。
線程異常處理
在線程創建時任何生效的try/catch/finally語句塊在線程開始執行后都與線程無關,線程的異常處理要在線程調用方法內部。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 try 6 { 7 new Thread(Test).Start(); 8 } 9 catch (Exception ex) 10 { 11 //代碼永遠不會運行到這里 12 Console.WriteLine(ex.Message); 13 } 14 Console.ReadKey(); 15 } 16 static void Test() 17 { 18 //要在方法內部捕獲異常 19 try 20 { 21 throw null; 22 } 23 catch (Exception ex) 24 { 25 //記錄日志等... 26 } 27 } 28 }
定時器&MemoryBarrier
1.定時器
.NET 提供了4種定時器
多線程計時器:
1:System.Threading.Timer
2:System.Timers.Timer
特殊目的的單線程計時器:
3:System.Windows.Forms.Timer(Windows Forms Timer)
4:System.Windows.Threading.DispatcherTimer(WPF timer);
有關定時器詳細介紹(個人覺得不錯):
https://www.cnblogs.com/LoveJenny/archive/2011/05/28/2053697.html
2.內存屏障 MemoryBarrier
編譯器、CLR或者CPU可能重新排序了程序指令,以此提高效率。同時引入緩存優化導致其他的線程不能馬上看到變量值的更改。lock可以滿足需要,但是競爭鎖會導致阻塞並且帶來上下文切換和調度等開銷,為此.NET 提供了非阻塞同步構造內存柵欄的概念。
有關MemoryBarrie詳細介紹(個人覺得不錯):
https://www.cnblogs.com/LoveJenny/archive/2011/05/29/2060718.html
允許我偷個懶 - -、 反正別人寫的也不錯。
線程組成要素
1.線程內核對象(thread kernel object)
包含對線程描述的屬性。還包含線程上下文(thread context)。上下文還包含CPU寄存器集合的內存塊(x86、x64、ARM CPU架構,線程上下文分別使用約700、1240、350字節的內存)。
2.線程環境塊(thread environment block,TEB)
TEB消耗一個內存頁(x86、x64和ARM CPU中是4KB)。包含線程異常處理鏈首(head)。線程進入的每個try塊都在鏈首插入一個節點(node);線程退出try塊時在鏈首中刪除對應節點。
3.用戶模式棧(user mode stack)
用戶模式棧存儲傳給方法的局部變量和實參。還包含一個地址用於指出當前方法返回時線程繼續執行位置(Winodws默認為用戶模式棧保留1MB地址空間,在線程實際需要時才會提交物理內存)。
4.DLL線程連接(attach)和線程分離(detach)通知
進程中線程在創建和終止時,都會調用線程中加載的所有非托管DLL的DllMain方法,並向該方法傳遞標記(DLL_THREAD_ATTACH或DLL_THREAD_DETACH)。有的DLL需要獲取這些通知,為進程中創建/銷毀的每個線程執行特殊的初始化或資源清理工作。
(C#和大多數托管編程語言生成的DLL沒有DllMain函數。所以托管DLL不會受到標志通知,非托管DLL可以調用Win32 DisableThreadLibraryCalls函數來決定不理會這些通知)
線程性能開銷
1.DLL線程鏈接與分離:目前隨便一個進程就可能加載幾百個DLL,每次開啟和銷毀一個線程這些函數都要調用一邊,嚴重影響了進程中創建和銷毀線程的性能。
2.線程上下文切換:單CPU計算機一次只做一件事情,所以Windwos必須在系統中的所有線程(邏輯CPU)之間共享物理CPU。
3.時間片切換:Windws只將一個線程分配給CPU.這個線程能運行一個“時間片”(量””,quantum)。時間片到期,winodws就上下文切換到另一個線程。每次上下文切換都要求Windws執行以下操作:
1.將CPU寄存器的值保存到當前正在運行的線程的內核對象內部的一個上下文結構中。
2.從現有線程集合中選出一個線程供調度。如果線程由另一個進程擁有,windows在開始執行任何代碼或者接觸任何數據之前,還必須切換CPU獲取到虛擬地址空間。
3.將所選上下文結構中的值加載到CPU的寄存器中。上下文切換完成后,CPU執行所選的線程,直到它的時間片到期。然后發生上下文切換。Windows大約每30米毫秒執行一次上下文切換。上下文切換是純開銷;不會換取任何內存和性能上的收益。
注意:
1.執行上下文切換所需的時間取決於CPU架構和速度。而填充CPU緩存所需的時間取決於系統中運行的應用程序、CPU緩存大小及其它因素。要構建高性能應用程序和組件,盡量避免上下文切換。
2.外垃圾回收時,CLR必須掛起所有線程,遍歷他們的棧來查找跟以便對堆中的對象進行標記,有的對象在壓縮期間發生了移動,所以要更新它的根,再回復所有線程。所以減少線程數量會提升垃圾回收的性能。
3.Winodws為每個進程提供了該進程專用的線程來增強系統的可靠性和影響力。在Winodws中,進程十分昂貴,創建一個進程通常需要花幾秒時間,必須分配大量內存,這些內存必須初始化,EXE和DLL文件必須從磁盤加載。相反在Winodws中創建線程則十分廉價。
結語
關於線程(Thread)你想知道應該都在這里了。
一個字:好累!
線程是一個很復雜的概念,延伸出來的知識點都需要有所了解,否則寫出的程序會出大問題(維護成本很高)。
參考文獻
CLR via C#(第4版) Jeffrey Richter
C#高級編程(第7版) Christian Nagel
C#高級編程(第10版) C# 6 & .NET Core 1.0 Christian Nagel
C# 經典實例 C# 6.0 &.NET Framework 4.6 Jay Hilyard
果殼中的C# C#5.0權威指南 Joseph Albahari
------------------------------------江湖救急 分割線----------------------------------------
求兩本書要中文版PDF(不知道目前有沒有賣紙質的?),哪位網友能否分享下,好人一生平安在此表示感謝!