一、概述程序員
最多見的併發場景包括:算法
編寫快速響應的用戶界面在WPF、移動應用和Windows Forms應用程序中,都須要併發執行耗時任務以保證用戶界面的響應性。編程
能夠處理同時出現的請求緩存
在服務器上,客戶端的請求可能會併發到達,必須經過並行處理纔可以保證程序的可伸縮性。若是使用ASP.NET、WCF或者WebServices,則.NET Framework會自動執行並行處理。然而,程序員仍然須要關注某些共享的狀態(例如使用靜態變量做爲緩存)。安全
並行編程服務器
若是能夠將負載劃分到多個核心上,那麼多核、多處理器計算機就能夠提高密集計算代碼的執行速度多線程
預測執行併發
在多核主機上,有時可經過預測的方式提早執行某些任務來改善程序性能。例如,LINQPad使用這種方式來提升查詢的建立速度。而若是事先沒法知道哪一種方法是最優的,則能夠並行執行多個解決同一任務的不一樣算法,最早完成的算法就是最優的。異步
這種程序同時執行代碼的機制稱爲多線程(multithreading)。CLR和操做系統都支持多線程,它是併發的基礎概念。所以,要介紹併發編程,首先就要具有線程的基礎知識,特別是線程的共享狀態。ide
二、線程
線程是一個能夠獨立執行的執行路徑。每個線程都運行在一個操做系統進程中。這個進程提供了程序執行的獨立環境。在單線程(single threaded)程序中,進程中只有一個線程運行,所以線程能夠獨立使用進程環境。而在多線程程序中,一個進程中會運行多個線程。它們共享同一個執行環境(特別是內存)。這在必定程度上說明了多線程的做用。例如,可使用一個線程在後臺得到數據,同時使用另外一個線程顯示所得到的數據。而這些數據就是所謂的共享狀態(shared state)。
1 //建立並啓動一個線程 須要首先實例化Thread 對象並調用Start方法
2 System.Threading.Thread th = new System.Threading.Thread(Print_y); 3 //啓動線程
4 th.Start();
1 //線程的執行狀態true || false
2 Console.WriteLine(th.IsAlive);
1 //獲取當前運行的線程的名稱
2 Console.WriteLine(System.Threading.Thread.CurrentThread.Name);
1 //建立並啓動一個線程 須要首先實例化Thread 對象並調用Start方法
2 System.Threading.Thread th = new System.Threading.Thread(Print_y); 3 //啓動線程
4 th.Start(); 5 //等待線程結束,能夠指定超時時間
6 th.Join(1000); 7
8 //暫停2秒鐘
9 System.Threading.Thread.Sleep(2000);
1 ////可使用ThreadState屬性測試線程的阻塞狀態
2 bool b = (th.ThreadState & System.Threading.ThreadState.WaitSleepJoin)!=0;
I/O密集和計算密集
若是一個操做的絕大部分時間都在等待事件的發生,則稱爲I/O密集,例以下載網頁或者調用Console.ReadLine。(I/O密集操做通常都會涉及輸入或者輸出,可是這並不是硬性要求。例如Thread.Sleep也是一種I/O密集的操做)。而相反的,若是操做的大部分時間都用於執行大量的CPU操做,則稱爲計算密集。
阻塞與自旋
I/O密集操做主要表現爲如下兩種形式:要麼在當前線程同步進行等待,直至操做完成(例如Console.ReadLine、Thread.Sleep以及Thread.Join);要麼異步進行操做,在操做完成的時候或者以後某個時刻觸發回調函數(以後將詳細介紹)。同步的I/O密集操做的大部分時間都花費在阻塞線程上,可是也可能在一個按期循環中自旋:
while(DataTime.Now<nextStartTime) { Thread.Sleep(1000); }
本地狀態與共享狀態
CLR爲每個線程分配了獨立的內存棧,從而保證了局部變量的隔離。下面的示例定義了一個擁有局部變量的方法,並同時在主線程和新建立的線程中調用該方法:
static void Main(string[] args) { //開啓一個新的線程
new System.Threading.Thread(Go).Start(); //主線程
Go(); Console.ReadKey(); } static void Go() { for (int count = 0; count < 5; count++) { Console.WriteLine("? "); } }
因爲每個線程的內存棧上都會有一個獨立的cycles變量的副本,所以咱們能夠預測,程序的輸出將是10個問號。
若是不一樣的線程擁有同一個對象的引用,則這些線程之間就共享了數據:
因爲兩個線程均在同一個ThreadTest實例上調用了Go()方法,所以它們共享_done字段。所以,「Done」只會打印一次,而非兩次。
鎖與線程安全
咱們能夠在讀寫共享字段時首先得到一個排它鎖來修正以前示例的問題。使用C#的lock語句就能夠實現這個目標:
class Program { static bool _done; static readonly object locker = new object(); static void Main(string[] args) { new System.Threading.Thread(()=>Go("Down")).Start(); Go("ss"); Console.ReadKey(); } public static void Go(string str) { lock (locker) { if (!_done) { Console.WriteLine(str); _done = true; } } } }
向線程傳遞數據
有時咱們須要給線程的啓動方法傳遞參數。最簡單的方案是使用Lambda表達式,並在其中使用指定的參數調用相應的方法。
Lambda表達式是在C#3.0以後引入的,你可能會遇到一些代碼使用了較老的技術,即向Thread的Start方法傳遞一個參數:
前臺線程與後臺線程
通常狀況下,顯式建立的線程稱爲前臺線程(Foreground thread)。只要有一個前臺線程還在運行,應用程序就仍然保持運行狀態。然後臺線程(background thread)則否則。當全部前臺線程結束時,應用程序就會中止,且全部運行的後臺線程也會隨之終止。
線程的前臺/後臺狀態和線程的優先級(執行時間的分配)無關。可使用線程的IsBackground屬性來查詢或修改線程的先後臺狀態:
信號發送
有時一個線程須要等待來自其餘線程的通知,即所謂的信號發送(signaling)。最簡單的信號發送結構是ManualResetEvent。調用ManualResetEvent的WaitOne方法能夠阻塞當前線程,直到其餘線程調用了Set「打開」了信號。如下的示例啓動了一個線程,並等待ManualResetEvent。它會阻塞兩秒鐘,直至主線程發送信號爲止:
//是否將初始值設置爲終止
var signal = new ManualResetEvent(false); new Thread(() => { Console.WriteLine("Waiting for signal...."); signal.WaitOne(); signal.Dispose(); Console.WriteLine("Got signal!"); }).Start(); Thread.Sleep(2000); signal.Set();
在Set調用後,信號發送結構仍然會保持「打開」狀態,能夠調用Reset方法再次將其「關閉」。
跨線程操做
//· 在WPF中,調用元素上的Dispatcher對象的BeginInvoke或Invoke方法。 //· 在UWP應用中,能夠調用Dispatcher對象的RunAsync或Invoke方法。 //· 在Windows Forms應用中:調用控件的BeginInvoke或Invoke方法。 private void button3_Click(object sender, EventArgs e) { Thread th = new Thread(Print); th.IsBackground = true; th.Start(); } public void Print() { if (textBox1.InvokeRequired)//是否對文本框進行跨線程訪問
{ //invoke:去找建立TextBox的線程
this.textBox1.Invoke(new Action<TextBox, string>(showTxt), this.textBox1, "123"); } else { textBox1.Text = "123"; } } public void showTxt(TextBox tet, string msg) { tet.Text = msg; }
線程池
在線程池上運行代碼的最簡單的方式是調用Task.Run(咱們將在下一節深刻介紹這個方法):
//在線程池上運行代碼的最簡單的方法是 Task.Run
Task.Run(() => { Console.WriteLine("hello world"); }); //.net4.0 以前沒有Task類 所以能夠調用下面的方法
ThreadPool.QueueUserWorkItem(n => Console.WriteLine(n));