本文是.NET異步和多線程系列第四章,主要介紹的是多線程異常處理、線程取消、多線程的臨時變量問題、線程安全和鎖lock等。html
多線程裏面拋出的異常,會終結當前線程,可是不會影響別的線程。那線程異常哪裏去了? 被吞了。c++
假如想獲取異常信息,這時候要怎麼辦呢?下面來看下其中的一種寫法(不推薦):數據庫
/// <summary> /// 1 多線程異常處理和線程取消 /// 2 多線程的臨時變量 /// 3 線程安全和鎖lock /// </summary> private void btnThreadCore_Click(object sender, EventArgs e) { Console.WriteLine($"****************btnThreadCore_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); #region 多線程異常處理 { try { List<Task> taskList = new List<Task>(); for (int i = 0; i < 100; i++) { string name = $"btnThreadCore_Click_{i}"; taskList.Add(Task.Run(() => { if (name.Equals("btnThreadCore_Click_11")) { throw new Exception("btnThreadCore_Click_11異常"); } else if (name.Equals("btnThreadCore_Click_12")) { throw new Exception("btnThreadCore_Click_12異常"); } else if (name.Equals("btnThreadCore_Click_38")) { throw new Exception("btnThreadCore_Click_38異常"); } Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); })); } //多線程裏面拋出的異常,會終結當前線程,可是不會影響別的線程。 //那線程異常哪裏去了? 被吞了。 //假如我想獲取異常信息,還須要通知別的線程 Task.WaitAll(taskList.ToArray()); //1 能夠捕獲到線程的異常 } catch (AggregateException aex) //2 須要try-catch-AggregateException { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } } catch (Exception ex) //能夠多catch 先具體再所有 { Console.WriteLine(ex); } //線程異常後常常是須要通知別的線程,而不是等到WaitAll,問題就是要線程取消? //工做中常規建議:多線程的委託裏面不容許異常,包一層try-catch,而後記錄下來異常信息,完成須要的操做。 } #endregion 多線程異常處理 Console.WriteLine($"****************btnThreadCore_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} " + $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); }
上面的這種寫法每每太極端了,一會兒捕獲了全部的異常。在真實工做中,線程異常後一般是須要通知別的線程(進行線程取消),而不是等到WaitAll。安全
工做中常規建議:多線程的委託裏面不容許異常,包一層try-catch,而後記錄下來異常信息,完成須要的操做。具體的咱們往下繼續看。多線程
多線程併發任務,某個失敗後,但願通知別的線程都停下來,要如何實現呢?閉包
Thread.Abort--終止線程;向當前線程拋一個異常而後終結任務;線程屬於OS資源,可能不會當即停下來。很是不建議這樣子去作,該方法如今也被微軟給廢棄了。併發
既然Task不能外部終止任務,那隻能本身終止本身(上帝才能戰勝本身),下面咱們來看下具體的代碼:(推薦)dom
#region 線程取消 { //多線程併發任務,某個失敗後,但願通知別的線程都停下來,要如何實現呢? //Thread.Abort--終止線程;向當前線程拋一個異常而後終結任務;線程屬於OS資源,可能不會當即停下來。很是不建議這樣子去作,該方法如今也被微軟給廢棄了。 //Task不能外部終止任務,只能本身終止本身(上帝才能戰勝本身) //cts有個bool屬性IsCancellationRequested 初始化是false //調用Cancel方法後變成true(不能再變回去),能夠重複Cancel try { CancellationTokenSource cts = new CancellationTokenSource(); List<Task> taskList = new List<Task>(); for (int i = 0; i < 50; i++) { string name = $"btnThreadCore_Click_{i}"; taskList.Add(Task.Run(() => { try { if (!cts.IsCancellationRequested) Console.WriteLine($"This is {name} 開始 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); Thread.Sleep(new Random().Next(50, 100)); if (name.Equals("btnThreadCore_Click_11")) { throw new Exception("btnThreadCore_Click_11異常"); } else if (name.Equals("btnThreadCore_Click_12")) { throw new Exception("btnThreadCore_Click_12異常"); } else if (name.Equals("btnThreadCore_Click_13")) { cts.Cancel(); } if (!cts.IsCancellationRequested) { Console.WriteLine($"This is {name}成功結束 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); } else { Console.WriteLine($"This is {name}中途中止 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); return; } } catch (Exception ex) { Console.WriteLine(ex.Message); cts.Cancel(); } }, cts.Token)); //加參數cts.Token目的是:在Cancel時尚未啓動的任務,就不啓動了。 //可是全部沒有啓動的任務都會拋出一個異常cts.Token.ThrowIfCancellationRequested } //1 準備cts 2 try-catch-cancel 3 Action要隨時判斷IsCancellationRequested //儘快中止,確定有延遲,在判斷環節纔會結束 Task.WaitAll(taskList.ToArray()); //若是線程還沒啓動,能不能就別啓動了?加參數cts.Token //1 啓動線程傳遞Token 2 異常抓取 //在Cancel時尚未啓動的任務,就不啓動了;也是拋異常,cts.Token.ThrowIfCancellationRequested } catch (AggregateException aex) { foreach (var exception in aex.InnerExceptions) { Console.WriteLine(exception.Message); } } catch (Exception ex) { Console.WriteLine(ex.Message); } } #endregion 線程取消
CancellationTokenSource有個bool屬性IsCancellationRequested,初始化是false,調用Cancel方法後變成true(不能再變回去),能夠重複Cancel。cts是線程安全的。異步
值得一提的是,使用Task.Run啓動線程的時候還傳了一個cts.Token的參數,目的是:調用Cancel方法後尚未啓動的任務,就不啓動了,實現原理是全部沒有啓動的任務都會拋出一個System.Threading.Tasks.TaskCanceledException類型的異常,異常描述爲「已取消一個任務」,拋出異常後任務天然也就終止了。通常狀況下咱們不會主動的去捕獲這種異常,this
那若是想看到這種異常信息的話能夠經過Task.WaitAll(taskList.ToArray())加上try{...}catch (AggregateException aex){...}這種方式去捕獲該類型的異常。
PS:能夠發現上面的這段代碼在線程內部的地方加了一個異常捕獲,工做中常規建議:多線程的委託裏面不容許異常,包一層try-catch,而後記錄下來異常信息,完成須要的操做。
注意:此處的線程中止也只能說是儘快中止,確定有延遲,在判斷環節纔會結束。
#region 多線程的臨時變量問題 { //多線程的臨時變量問題,線程是非阻塞的,延遲啓動的;線程執行的時候,i已是5了。 for (int i = 0; i < 5; i++) { Task.Run(() => { //此處i都是5 Console.WriteLine($"This is btnThreadCore_Click_{i} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); } //k是閉包裏面的變量,每次循環都有一個獨立的k //5個k變量 1個i變量 for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { Console.WriteLine($"This is btnThreadCore_Click_{i}_{k} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}"); }); } } #endregion 多線程的臨時變量問題
運行結果以下:
線程安全:若是你的代碼在進程中有多個線程同時運行這一段,若是每次運行的結果都跟單線程運行時的結果一致,那麼就是線程安全的。
線程安全問題通常都是有全局變量/共享變量/靜態變量/硬盤文件/數據庫的值,只要是多線程都能訪問和修改的就有多是非線程安全。
非線程安全是由於多個線程相同操做,出現了覆蓋,那要怎麼解決?
方案1:使用lock解決多線程衝突(如今通常不推薦使用這個,會限制併發)
lock是語法糖,Monitor.Enter,佔據一個引用,別的線程就只能等着。
推薦鎖是private static readonly object lockObj = new object();
首先咱們來看下lock的標準寫法:
//字段 private static readonly object lockObj = new object(); private int iNumSync = 0; private int iNumAsync = 0; //非線程安全 private int iNumLockAsync = 0; private List<int> iListAsync = new List<int>();
{ for (int i = 0; i < 10000; i++) { this.iNumSync++; //單線程 } for (int i = 0; i < 10000; i++) { Task.Run(() => { this.iNumAsync++; //非線程安全 }); } for (int i = 0; i < 10000; i++) { Task.Run(() => { //lock的標準寫法 //推薦鎖是private static readonly object lockObj = new object(); lock (lockObj) //任意時刻只有一個線程能進入方法塊,這不就變成了單線程,限制了併發 { this.iNumLockAsync++; } }); } for (int i = 0; i < 10000; i++) { int k = i; Task.Run(() => this.iListAsync.Add(k)); //非線程安全 } Thread.Sleep(5 * 1000); Console.WriteLine($"iNumSync={this.iNumSync} iNumAsync={this.iNumAsync} iNumLockAsync={iNumLockAsync} listNum={this.iListAsync.Count}"); //結果:iNumSync=1000 、 iNumAsync=1到1000之間 、 iNumLockAsync=1000 、 this.iListAsync.Count=1到1000之間 }
運行結果以下:
使用lock雖然能夠解決線程安全問題,可是同時也限制了併發。
使用lock的注意點:
A 不能是lock(null),能夠編譯但不能運行;
B 不推薦lock(this),外面若是也要用實例,就衝突了;
C 不該該是lock(string字符串),string在內存分配上是重用的,會衝突;
D lock裏面的代碼不要太多,這裏是單線程的;
下面咱們來看些例子:
爲何不推薦lock(this)?
public class Test { private int iDoTestNum = 0; private string name = "浪子天涯"; /// <summary> /// 鎖this會和外部鎖對象實例衝突 /// </summary> public void DoTest() { //遞歸調用,lock (this) 會不會死鎖? 正確答案是不會死鎖! //這裏是同一個線程,這個引用就是被這個線程所佔據。 lock (this) { Thread.Sleep(500); this.iDoTestNum++; if (this.iDoTestNum < 10) { Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}"); this.DoTest(); } else { Console.WriteLine("28號,課程結束!!"); } } } /// <summary> /// 這次鎖字符串會和外部鎖值相同的字符串衝突 /// 這是由於相同的字符串會被指向同一塊引用,這就至關於鎖同一個引用,即同一個鎖 /// </summary> public void DoTestString() { //這次不會死鎖 //這裏是同一個線程,這個引用就是被這個線程所佔據。 lock (this.name) { Thread.Sleep(500); this.iDoTestNum++; if (this.iDoTestNum < 10) { Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}"); this.DoTestString(); } else { Console.WriteLine("28號,課程結束!!"); } } } }
#region 線程安全和鎖lock { //線程安全:若是你的代碼在進程中有多個線程同時運行這一段,若是每次運行的結果都跟單線程運行時的結果一致,那麼就是線程安全的。 //線程安全問題通常都是有全局變量/共享變量/靜態變量/硬盤文件/數據庫的值,只要是多線程都能訪問和修改的就有多是非線程安全。 //非線程安全是由於多個線程相同操做,出現了覆蓋,那要怎麼解決? //一、使用lock解決多線程衝突 //lock是語法糖,Monitor.Enter,佔據一個引用,別的線程就只能等着。 //推薦鎖是private static readonly object lockObj = new object(); //A 不能是lock(null),能夠編譯但不能運行; //B 不推薦lock(this),外面若是也要用實例,就衝突了; //C 不該該是lock(string字符串),string在內存分配上是重用的,會衝突; //D lock裏面的代碼不要太多,這裏是單線程的; Test test = new Test(); Task.Delay(1000).ContinueWith(t => { lock (test) //和Test內部的lock(this)是同一個鎖,故這次儘管是子線程也要排隊等待 { Console.WriteLine("*********lock(this) Start*********"); Thread.Sleep(2000); Console.WriteLine("*********lock(this) End*********"); } }); test.DoTest(); } #endregion 線程安全和鎖lock
運行結果以下:
仔細觀察會發現Task子線程的任務會等到test.DoTest()的任務執行完後纔會執行,這是爲何呢?
有些人可能就會有疑問了,此處鎖this和鎖test實例看上去應該是2把鎖,互不影響纔對啊,那爲何又會衝突呢?
實際上此處的this和test是同一個實例,那麼鎖的固然也是同一個引用,故至關因而同一把鎖。
那又爲何不該該鎖string字符串呢?
咱們在上面的例子上作一些調整以下所示:
#region 線程安全和鎖lock { //線程安全:若是你的代碼在進程中有多個線程同時運行這一段,若是每次運行的結果都跟單線程運行時的結果一致,那麼就是線程安全的。 //線程安全問題通常都是有全局變量/共享變量/靜態變量/硬盤文件/數據庫的值,只要是多線程都能訪問和修改的就有多是非線程安全。 //非線程安全是由於多個線程相同操做,出現了覆蓋,那要怎麼解決? //一、使用lock解決多線程衝突 //lock是語法糖,Monitor.Enter,佔據一個引用,別的線程就只能等着。 //推薦鎖是private static readonly object lockObj = new object(); //A 不能是lock(null),能夠編譯但不能運行; //B 不推薦lock(this),外面若是也要用實例,就衝突了; //C 不該該是lock(string字符串),string在內存分配上是重用的,會衝突; //D lock裏面的代碼不要太多,這裏是單線程的; { // Test test = new Test(); // Task.Delay(1000).ContinueWith(t => // { // lock (test) //和Test內部的lock(this)是同一個鎖,故這次儘管是子線程也要排隊等待 // { // Console.WriteLine("*********lock(this) Start*********"); // Thread.Sleep(2000); // Console.WriteLine("*********lock(this) End*********"); // } // }); // test.DoTest(); } { Test test = new Test(); string student = "浪子天涯"; Task.Delay(1000).ContinueWith(t => { lock (student) { Console.WriteLine("*********lock(string) Start*********"); Thread.Sleep(2000); Console.WriteLine("*********lock(string) End*********"); } }); test.DoTestString(); } } #endregion 線程安全和鎖lock
運行結果以下:
仔細觀察會發現這和lock(this)的效果是同樣的,那這又是爲何呢?
這是因爲C#內存分配致使的,相同的字符串會被指向同一塊引用空間,那麼此處的鎖this.name變量和鎖student變量就至關於鎖同一個引用,故至關因而同一把鎖。
方案2:線程安全集合
使用System.Collections.Concurrent.ConcurrentQueue<int>等相關操做,System.Collections.Concurrent命名空間下的相關操做是線程安全的。
方案3:數據分拆,避免多線程操做同一個數據,又安全又高效(推薦)
在真實工做中遇到線程不安全的狀況,若是有辦法使用數據分拆來解決則推薦使用數據分拆,數據分拆沒法解決的時候再考慮使用鎖。
Demo源碼:
連接:https://pan.baidu.com/s/1Eaet92HhGoK9sHjXhz_VsA 提取碼:7st0
此文由博主精心撰寫轉載請保留此原文連接:https://www.cnblogs.com/xyh9039/p/13592042.html