一、多線程編程必備知識編程
1.1 進程與線程的概念多線程
當咱們打開一個應用程序後,操做系統就會爲該應用程序分配一個進程ID,例如打開QQ,你將在任務管理器的進程選項卡看到QQ.exe進程,以下圖:異步
進程能夠理解爲一塊包含了某些資源的內存區域,操做系統經過進程這一方式把它的工做劃分爲不一樣的單元。一個應用程序能夠對應於多個進程。函數
線程是進程中的獨立執行單元,對於操做系統而言,它經過調度線程來使應用程序工做,一個進程中至少包含一個線程,咱們把該線程成爲主線程。線程與進程之間的關係能夠理解爲:線程是進程的執行單元,操做系統經過調度線程來使應用程序工做;而進程則是線程的容器,它由操做系統建立,又在具體的執行過程當中建立了線程。性能
1.2 線程的調度spa
在操做系統的書中貌似有提過,「Windows是搶佔式多線程操做系統」。之因此這麼說它是搶佔式的,是由於線程能夠在任意時間裏被搶佔,來調度另外一個線程。操做系統爲每一個線程分配了0-31中的某一級優先級,並且會把優先級高的線程優先分配給CPU執行。操作系統
Windows支持7個相對線程優先級:Idle、Lowest、BelowNormal、Normal、AboveNormal、Highest和Time-Critical。其中,Normal是默認的線程優先級。程序能夠經過設置Thread的Priority屬性來改變線程的優先級,該屬性的類型爲ThreadPriority枚舉類型,其成員包括Lowest、BelowNormal、Normal、AboveNormal和Highest。CLR爲本身保留了Idle和Time-Critical兩個優先級。pwa
1.3 線程也分先後臺線程
線程有前臺線程和後臺線程之分。在一個進程中,當全部前臺線程中止運行後,CLR會強制結束全部仍在運行的後臺線程,這些後臺線程被直接終止,卻不會拋出任何異常。主線程將一直是前臺線程。咱們可使用Tread類來建立前臺線程。code
1 using System; 2 using System.Threading; 3 4 namespace 多線程1 5 { 6 internal class Program 7 { 8 private static void Main(string[] args) 9 { 10 var backThread = new Thread(Worker); 11 backThread.IsBackground = true; 12 backThread.Start(); 13 Console.WriteLine("從主線程退出"); 14 Console.ReadKey(); 15 } 16 17 private static void Worker() 18 { 19 Thread.Sleep(1000); 20 Console.WriteLine("從後臺線程退出"); 21 } 22 } 23 }
以上代碼先經過Thread類建立了一個線程對象,而後經過設置IsBackground屬性來指明該線程爲後臺線程。若是不設置這個屬性,則默認爲前臺線程。接着調用了Start的方法,此時後臺線程會執行Worker函數的代碼。因此在這個程序中有兩個線程,一個是運行Main函數的主線程,一個是運行Worker線程的後臺線程。因爲前臺線程執行完畢後CLR會無條件地終止後臺線程的運行,因此在前面的代碼中,若啓動了後臺線程,則主線程將會繼續運行。主線程執行完後,CLR發現主線程結束,會終止後臺線程,而後使整個應用程序結束運行,因此Worker函數中的Console語句將不會執行。因此上面代碼的結果是不會運行Worker函數中的Console語句的。
可使用Join函數的方法,確保主線程會在後臺線程執行結束後纔開始運行。
1 using System; 2 using System.Threading; 3 4 namespace 多線程1 5 { 6 internal class Program 7 { 8 private static void Main(string[] args) 9 { 10 var backThread = new Thread(Worker); 11 backThread.IsBackground = true; 12 backThread.Start(); 13 backThread.Join(); 14 Console.WriteLine("從主線程退出"); 15 Console.ReadKey(); 16 } 17 18 private static void Worker() 19 { 20 Thread.Sleep(1000); 21 Console.WriteLine("從後臺線程退出"); 22 } 23 } 24 }
以上代碼調用Join函數來確保主線程會在後臺線程結束後再運行。
若是你線程執行的方法須要參數,則就須要使用new Thread的重載構造函數Thread(ParameterizedThreadStart).
1 using System; 2 using System.Threading; 3 4 namespace 多線程1 5 { 6 internal class Program 7 { 8 private static void Main(string[] args) 9 { 10 var backThread = new Thread(new ParameterizedThreadStart(Worker)); 11 backThread.IsBackground = true; 12 backThread.Start("Helius"); 13 backThread.Join(); 14 Console.WriteLine("從主線程退出"); 15 Console.ReadKey(); 16 } 17 18 private static void Worker(object data) 19 { 20 Thread.Sleep(1000); 21 Console.WriteLine($"傳入的參數爲{data.ToString()}"); 22 } 23 } 24 }
執行結果爲:
二、線程的容器——線程池
前面咱們都是經過Thead類來手動建立線程的,然而線程的建立和銷燬會耗費大量時間,這樣的手動操做將形成性能損失。所以,爲了不因經過Thread手動建立線程而形成的損失,.NET引入了線程池機制。
2.1 線程池
線程池是指用來存放應用程序中要使用的線程集合,能夠將它理解爲一個存放線程的地方,這種集中存放的方式有利於對線程進行管理。
CLR初始化時,線程池中是沒有線程的。在內部,線程池維護了一個操做請求隊列,當應用程序想要執行一個異步操做時,須要調用QueueUserWorkItem方法來將對應的任務添加到線程池的請求隊列中。線程池實現的代碼會從隊列中提取,並將其委派給線程池中的線程去執行。若是線程池沒有空閒的線程,則線程池也會建立一個新線程去執行提取的任務。而當線程池線程完成某個任務時,線程不會被銷燬,而是返回到線程池中,等待響應另外一個請求。因爲線程不會被銷燬,因此也就避免了性能損失。記住,線程池裏的線程都是後臺線程,默認級別是Normal。
2.2 經過線程池來實現多線程
要使用線程池的線程,須要調用靜態方法ThreadPool.QueueUserWorkItem,以指定線程要調用的方法,該靜態方法有兩個重載版本:
public static bool QueueUserWorkItem(WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callback,Object state)
這兩個方法用於向線程池隊列添加一個工做先以及一個可選的狀態數據。而後,這兩個方法就會當即返回。下面經過實例來演示如何使用線程池來實現多線程編程。
1 using System; 2 using System.Threading; 3 4 namespace 多線程2 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Console.WriteLine($"主線程ID={Thread.CurrentThread.ManagedThreadId}"); 11 ThreadPool.QueueUserWorkItem(CallBackWorkItem); 12 ThreadPool.QueueUserWorkItem(CallBackWorkItem,"work"); 13 Thread.Sleep(3000); 14 Console.WriteLine("主線程退出"); 15 Console.ReadKey(); 16 } 17 18 private static void CallBackWorkItem(object state) 19 { 20 Console.WriteLine("線程池線程開始執行"); 21 if (state != null) 22 { 23 Console.WriteLine($"線程池線程ID={Thread.CurrentThread.ManagedThreadId},傳入的參數爲{state.ToString()}"); 24 } 25 else 26 { 27 Console.WriteLine($"線程池線程ID={Thread.CurrentThread.ManagedThreadId}"); 28 } 29 } 30 } 31 }
結果爲:
2.3 協做式取消線程池線程
.NET Framework提供了取消操做的模式,這個模式是協做式的。爲了取消一個操做,必須建立一個System.Threading.CancellationTokenSource對象。下面仍是使用代碼來演示一下:
using System; using System.Threading; namespace 多線程3 { internal class Program { private static void Main(string[] args) { Console.WriteLine("主線程運行"); var cts = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(Callback, cts.Token); Console.WriteLine("按下回車鍵來取消操做"); Console.Read(); cts.Cancel(); Console.ReadKey(); } private static void Callback(object state) { var token = (CancellationToken) state; Console.WriteLine("開始計數"); Count(token, 1000); } private static void Count(CancellationToken token, int count) { for (var i = 0; i < count; i++) { if (token.IsCancellationRequested) { Console.WriteLine("計數取消"); return; } Console.WriteLine($"計數爲:{i}"); Thread.Sleep(300); } Console.WriteLine("計數完成"); } } }
結果爲:
三、線程同步
線程同步計數是指多線程程序中,爲了保證後者線程,只有等待前者線程完成以後才能繼續執行。這就比如生活中排隊買票,在前面的人沒買到票以前,後面的人必須等待。
3.1 多線程程序中存在的隱患
多線程可能同時去訪問一個共享資源,這將損壞資源中所保存的數據。這種狀況下,只能採用線程同步技術。
3.2 使用監視器對象實現線程同步
監視器對象(Monitor)可以確保線程擁有對共享資源的互斥訪問權,C#經過lock關鍵字來提供簡化的語法。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace 線程同步 9 { 10 class Program 11 { 12 private static int tickets = 100; 13 static object globalObj=new object(); 14 static void Main(string[] args) 15 { 16 Thread thread1=new Thread(SaleTicketThread1); 17 Thread thread2=new Thread(SaleTicketThread2); 18 thread1.Start(); 19 thread2.Start(); 20 Console.ReadKey(); 21 } 22 23 private static void SaleTicketThread2() 24 { 25 while (true) 26 { 27 try 28 { 29 Monitor.Enter(globalObj); 30 Thread.Sleep(1); 31 if (tickets > 0) 32 { 33 Console.WriteLine($"線程2出票:{tickets--}"); 34 } 35 else 36 { 37 break; 38 } 39 } 40 catch (Exception) 41 { 42 throw; 43 } 44 finally 45 { 46 Monitor.Exit(globalObj); 47 } 48 } 49 } 50 51 private static void SaleTicketThread1() 52 { 53 while (true) 54 { 55 try 56 { 57 Monitor.Enter(globalObj); 58 Thread.Sleep(1); 59 if (tickets > 0) 60 { 61 Console.WriteLine($"線程1出票:{tickets--}"); 62 } 63 else 64 { 65 break; 66 } 67 } 68 catch (Exception) 69 { 70 throw; 71 } 72 finally 73 { 74 Monitor.Exit(globalObj); 75 } 76 } 77 } 78 } 79 }
在以上代碼中,首先額外定義了一個靜態全局變量globalObj,並將其做爲參數傳遞給Enter方法。使用了Monitor鎖定的對象須要爲引用類型,而不能爲值類型。由於在將值類型傳遞給Enter時,它將被先裝箱爲一個單獨的毒香,以後再傳遞給Enter方法;而在將變量傳遞給Exit方法時,也會建立一個單獨的引用對象。此時,傳遞給Enter方法的對象和傳遞給Exit方法的對象不一樣,Monitor將會引起SynchronizationLockException異常。
3.3 線程同步技術存在的問題
(1)使用比較繁瑣。要用額外的代碼把多個線程同時訪問的數據包圍起來,還並不能遺漏。
(2)使用線程同步會影響程序性能。由於獲取和釋放同步鎖是須要時間的;而且決定那個線程先得到鎖的時候,CPU也要進行協調。這些額外的工做都會對性能形成影響。
(3)線程同步每次只容許一個線程訪問資源,這會致使線程堵塞。繼而系統會建立更多的線程,CPU也就要負擔更繁重的調度工做。這個過程會對性能形成影響。
下面就由代碼來解釋一下性能的差距:
1 using System; 2 using System.Collections.Generic; 3 using System.Diagnostics; 4 using System.Linq; 5 using System.Text; 6 using System.Threading; 7 using System.Threading.Tasks; 8 9 namespace 線程同步2 10 { 11 class Program 12 { 13 static void Main(string[] args) 14 { 15 int x = 0; 16 const int iterationNumber = 5000000; 17 Stopwatch stopwatch=Stopwatch.StartNew(); 18 for (int i = 0; i < iterationNumber; i++) 19 { 20 x++; 21 } 22 Console.WriteLine($"不使用鎖的狀況下花費的時間:{stopwatch.ElapsedMilliseconds}ms"); 23 stopwatch.Restart(); 24 for (int i = 0; i < iterationNumber; i++) 25 { 26 Interlocked.Increment(ref x); 27 } 28 Console.WriteLine($"使用鎖的狀況下花費的時間:{stopwatch.ElapsedMilliseconds}ms"); 29 Console.ReadKey(); 30 } 31 } 32 }
執行結果:
實踐出結論。