本文是.NET異步和多執行緒系列第四章,主要介紹的是多執行緒例外處理、執行緒取消、多執行緒的臨時變數問題、執行緒安全和鎖lock等,
一、多執行緒例外處理
多執行緒里面拋出的例外,會終結當前執行緒,但是不會影響別的執行緒,那執行緒例外哪里去了? 被吞了,
假如想獲取例外資訊,這時候要怎么辦呢?下面來看下其中的一種寫法(不推薦):
/// <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不能外部終止任務,那只能自己終止自己(上帝才能打敗自己),下面我們來看下具體的代碼:(推薦)
#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型別的例外,例外描述為“已取消一個任務”,拋出例外后任務自然也就終止了,一般情況下我們不會主動的去捕獲這種例外,
那如果想看到這種例外資訊的話可以通過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 多執行緒的臨時變數問題
運行結果如下:

四、執行緒安全和鎖lock
執行緒安全:如果你的代碼在行程中有多個執行緒同時運行這一段,如果每次運行的結果都跟單執行緒運行時的結果一致,那么就是執行緒安全的,
執行緒安全問題一般都是有全域變數/共享變數/靜態變數/硬碟檔案/資料庫的值,只要是多執行緒都能訪問和修改的就有可能是非執行緒安全,
非執行緒安全是因為多個執行緒相同操作,出現了覆寫,那要怎么解決?
方案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 { //執行緒安全:如果你的代碼在行程中有多個執行緒同時運行這一段,如果每次運行的結果都跟單執行緒運行時的結果一致,那么就是執行緒安全的, //執行緒安全問題一般都是有全域變數/共享變數/靜態變數/硬碟檔案/資料庫的值,只要是多執行緒都能訪問和修改的就有可能是非執行緒安全, //非執行緒安全是因為多個執行緒相同操作,出現了覆寫,那要怎么解決? //1、使用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 { //執行緒安全:如果你的代碼在行程中有多個執行緒同時運行這一段,如果每次運行的結果都跟單執行緒運行時的結果一致,那么就是執行緒安全的, //執行緒安全問題一般都是有全域變數/共享變數/靜態變數/硬碟檔案/資料庫的值,只要是多執行緒都能訪問和修改的就有可能是非執行緒安全, //非執行緒安全是因為多個執行緒相同操作,出現了覆寫,那要怎么解決? //1、使用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
著作權宣告:如有雷同純屬巧合,如有侵權請及時聯系本人修改,謝謝!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/1609.html
標籤:ASP.NET
