線程(thread)

線程概述

線程是一個獨立處理的執行路徑。每一個線程都運行在一個操做系統進程中,這個進程是程序執行的獨立環境。在單線程中進程的獨立環境內只有一個線程運行,因此該線程具備獨立使用進程資源的權利。在多線程程序中,在進程中有多個線程運行,因此它們共享同一個執行環境。html

 

基礎線程(thread)

使用Thread類能夠建立和控制線程,定義在System.Threading命名空間中:node

 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毫秒),用於模擬併發性。而在多核/多處理器主機上線程卻可以真正實現並行執行(分別由計算機上其它激活處理器完成)。windows

 

線程經常使用方法

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 正常
普通線程的優先級默認爲Normal,主線程和其它工做線程(默認優先級)優先級相同,交替進行。
注意:線程優先級跟線程執行的前後順序無關,而是肯定其激活線程在操做系統中的相對執行時間的長短(肯定分配」時間片「的長短,即線程執行時間長短)。

ThreadStart&ParameterizedThreadStart

Thread重載的其它四種構造函數須要帶入特殊對象,分別是ThreadStartParameterizedThreadStart類。

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     }

執行結果(執行結果不固定):

由於ThreadStartParameterizedThreadStart委託,因此咱們也能夠把符合要求的自定義委託或者內置委託進行轉換帶入構造函數。例如:

 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     }

上述案例對比DemoOneDemoTwo的執行結果咱們能夠獲得:

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.EnterMonitor.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的信號量與Mutexlock類似,可是信號量與線程無關,任何線程均可以釋放,而Mutexlock,只有得到鎖的線程才能夠釋放。

下面示例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     }

信號量可限制併發處理,防止太多線程同時執行特定代碼。這個類有兩個功能類似的版本:SemaphoreSemaphoreSlim。後者是.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也提供了相應的方法爲TryEnterReadLockTryEnterWriteLock

 

發送信號(ManualResetEvent、AutoResetEvent、CountdownEvent和Barrier)

發送信號包括ManualResetEvent(Slim)AutoResetEventCountdownEventBarrier

前三個就是所謂的事件等待處理器(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類的兩個方法GetDataSetData。這兩個方法會將數據存儲在線程獨有的「插槽」中。須要使用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寄存器集合的內存塊(x8六、x6四、ARM CPU架構,線程上下文分別使用約700、1240、350字節的內存)。

2.線程環境塊(thread environment block,TEB)

TEB消耗一個內存頁(x8六、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(不知道目前有沒有賣紙質的?),哪位網友可否分享下,好人一輩子平安在此表示感謝!

                                

相關文章
相關標籤/搜索