C#多線程的相關(guān)操作講解
一、線程異常
我們?cè)趩尉€程中,捕獲異??梢允褂胻ry-catch,代碼如下所示:
using System; namespace MultithreadingOption { class Program { static void Main(string[] args) { #region 單線程中捕獲異常 try { int[] array = { 1, 23, 61, 678, 23, 45 }; Console.WriteLine(array[6]); } catch (Exception ex) { Console.WriteLine($"message:{ex.Message}"); } #endregion Console.ReadKey(); } } }
程序運(yùn)行結(jié)果:
那么在多線程中如何捕獲異常呢?是不是也可以使用try-catch進(jìn)行捕獲?我們先看下面的代碼:
using System; using System.Threading.Tasks; namespace MultithreadingOption { class Program { static void Main(string[] args) { #region 單線程中捕獲異常 //try //{ // int[] array = { 1, 23, 61, 678, 23, 45 }; // Console.WriteLine(array[6]); //} //catch (Exception ex) //{ // Console.WriteLine($"message:{ex.Message}"); //} #endregion #region 多線程中的異常 try { for (int i = 0; i < 30; i++) { string str = $"main_{i}"; // 開(kāi)啟線程 Task.Run(() => { Console.WriteLine($"{str} 開(kāi)始了"); if(str.Equals("main_5")) { throw new Exception("main_5 發(fā)生了異常"); } else if (str.Equals("main_11")) { throw new Exception("main_11 發(fā)生了異常"); } else if (str.Equals("main_18")) { throw new Exception("main_18 發(fā)生了異常"); } Console.WriteLine($"{str} 結(jié)束了"); }); } } catch (Exception ex) { Console.WriteLine($"message:{ex.Message}"); } #endregion Console.ReadKey(); } } }
程序運(yùn)行結(jié)果:
我們看到結(jié)果中并沒(méi)有輸出異常信息,是不是沒(méi)有拋出異常呢?我們起代碼進(jìn)行調(diào)試,看調(diào)試信息:
我們看到程序中確實(shí)也拋出了異常,但是程序卻沒(méi)有捕獲到,那么異常去哪里了呢?異常被多線程給吞掉了,那么如何在多線程中捕獲異常呢?如果把try-catch寫(xiě)在線程里面呢?每一個(gè)線程都是單線程的,把try-catch寫(xiě)在每一個(gè)線程里面就沒(méi)有意義了。在多線程中捕獲異常,需要使用到WaitAll(),看下面的代碼:
try { // 定義一個(gè)Task類(lèi)型的List集合 List<Task> taskList = new List<Task>(); for (int i = 0; i < 30; i++) { string str = $"main_{i}"; // 開(kāi)啟線程,并把線程添加到集合中 taskList.Add(Task.Run(() => { Console.WriteLine($"{str} 開(kāi)始了"); if (str.Equals("main_5")) { throw new Exception("main_5 發(fā)生了異常"); } else if (str.Equals("main_11")) { throw new Exception("main_11 發(fā)生了異常"); } else if (str.Equals("main_18")) { throw new Exception("main_18 發(fā)生了異常"); } Console.WriteLine($"{str} 結(jié)束了"); })); } // 等待所有線程都執(zhí)行完 Task.WaitAll(taskList.ToArray()); } catch (Exception ex) { Console.WriteLine($"message:{ex.Message}"); }
我們用代碼進(jìn)行調(diào)試,調(diào)試結(jié)果:
這時(shí)就可以進(jìn)入到catch里面了,我們監(jiān)視ex,發(fā)現(xiàn)ex是AggregateException類(lèi)型的異常,我們?cè)谶M(jìn)一步優(yōu)化代碼:
try { // 定義一個(gè)Task類(lèi)型的List集合 List<Task> taskList = new List<Task>(); for (int i = 0; i < 30; i++) { string str = $"main_{i}"; // 開(kāi)啟線程,并把線程添加到集合中 taskList.Add(Task.Run(() => { Console.WriteLine($"{str} 開(kāi)始了"); if (str.Equals("main_5")) { throw new Exception("main_5 發(fā)生了異常"); } else if (str.Equals("main_11")) { throw new Exception("main_11 發(fā)生了異常"); } else if (str.Equals("main_18")) { throw new Exception("main_18 發(fā)生了異常"); } Console.WriteLine($"{str} 結(jié)束了"); })); } // 等待所有線程都執(zhí)行完 Task.WaitAll(taskList.ToArray()); } catch(AggregateException are) { foreach (var exception in are.InnerExceptions) { Console.WriteLine(exception.Message); } } catch (Exception ex) { Console.WriteLine($"message:{ex.Message}"); }
最后運(yùn)行程序:
我們發(fā)現(xiàn)這時(shí)就可以捕獲到具體的異常信息了。
二、線程取消
在上面的示例中,我們捕獲到了多線程中發(fā)生的異常,并且也輸出了異常信息,但是這樣是不友好的。在實(shí)際開(kāi)發(fā)中,我們使用多線程并發(fā)執(zhí)行任務(wù),假如其中某一個(gè)任務(wù)失敗了或者發(fā)生了異常,我們希望可以通知其他的線程,都停止下來(lái),那么該如何做呢?這時(shí)就需要使用到線程取消。
Task不能外部終止任務(wù),只能自己終止自己。
.Net框架提供了CancellationTokenSource類(lèi),該類(lèi)里面有一個(gè)bool類(lèi)型的屬性:IsCancellationRequested,默認(rèn)是false,表示是否取消線程。還提供了一個(gè)Cancel()方法,該方法可以把IsCancellationRequested的屬性值設(shè)置為true,并且不能在設(shè)置回去。代碼如下:
// 實(shí)例化對(duì)象 CancellationTokenSource cts = new CancellationTokenSource(); for (int i = 0; i < 20; i++) { string str = $"main_{i}"; // 開(kāi)啟線程 Task.Run(() => { try { Console.WriteLine($"{str} 開(kāi)始了"); // 暫停 Thread.Sleep(new Random().Next(50, 100) * 100); if (str.Equals("main_5")) { throw new Exception("main_5 發(fā)生了異常"); } else if (str.Equals("main_11")) { throw new Exception("main_11 發(fā)生了異常"); } if (cts.IsCancellationRequested == false) { Console.WriteLine($"{str} 結(jié)束了"); } else { Console.WriteLine($"{str} 線程取消"); } } catch (Exception ex) { // 發(fā)生了異常,將IsCancellationRequested的值設(shè)置為true cts.Cancel(); Console.WriteLine($"message:{ex.Message}"); } }); }
程序運(yùn)行結(jié)果:
可以看到,當(dāng)有異常發(fā)生之后,有的線程就被取消了。這樣就初步實(shí)現(xiàn)了線程取消。
在上面的示例中,我們是先開(kāi)啟了線程,如果發(fā)生了異常,則取消線程。那么會(huì)有這樣一種情況:線程中發(fā)生了異常,可能這時(shí)候有的線程還沒(méi)有開(kāi)啟,那么能不能就不讓這些線程在開(kāi)啟呢?Task的Run方法有一個(gè)重載:
第二個(gè)參數(shù)就表示取消線程。而且CancellationTokenSource類(lèi)里面正好有這個(gè)參數(shù):
所以,我們可以利用Run方法的重載來(lái)實(shí)現(xiàn)不開(kāi)啟線程,代碼如下:
try { // 實(shí)例化對(duì)象 CancellationTokenSource cts = new CancellationTokenSource(); // 創(chuàng)建Task類(lèi)型的集合 List<Task> taskList = new List<Task>(); for (int i = 0; i < 20; i++) { string str = $"main_{i}"; // 開(kāi)啟線程 Task.run 以后 添加Token 就可以在某一個(gè)線程發(fā)生異常之后,讓沒(méi)有開(kāi)啟的線程不開(kāi)啟了 taskList.Add(Task.Run(() => { try { Console.WriteLine($"{str} 開(kāi)始了"); // 暫停 Thread.Sleep(new Random().Next(50, 100) * 10); if (str.Equals("main_5")) { throw new Exception("main_5 發(fā)生了異常"); } else if (str.Equals("main_11")) { throw new Exception("main_11 發(fā)生了異常"); } if (cts.IsCancellationRequested == false) { Console.WriteLine($"{str} 結(jié)束了"); } else { Console.WriteLine($"{str} 線程取消"); } } catch (Exception ex) { // 發(fā)生了異常,將IsCancellationRequested的值設(shè)置為true cts.Cancel(); } }, cts.Token)); } // 等待所有線程執(zhí)行完 Task.WaitAll(taskList.ToArray()); } catch (AggregateException are) { foreach (var exception in are.InnerExceptions) { Console.WriteLine(exception.Message); } }
程序運(yùn)行結(jié)果:
輸出結(jié)果中有一句話:已取消一個(gè)任務(wù),但是我們的代碼里面沒(méi)有打印這句話,這是從哪里來(lái)的呢?這是因?yàn)榈诙€(gè)參數(shù)Token的原因,加了這個(gè)參數(shù)以后,如果就線程發(fā)生了異常,就不在繼續(xù)開(kāi)啟線程。
三、臨時(shí)變量
我們先來(lái)看看下面一段代碼:
for (int i = 0; i < 20; i++) { // 開(kāi)啟線程 Task.Run(() => { Task.Run(() => Console.WriteLine($"this is {i} ThreadId: {Thread.CurrentThread.ManagedThreadId.ToString("00")}")); }); }
這段代碼的輸出結(jié)果是什么呢?我們運(yùn)行程序查看結(jié)果:
可能有人會(huì)感到疑惑:為什么輸出的都是20呢,而不是每次循環(huán)變量的值?這是什么原因呢。這是因?yàn)槲覀兩暾?qǐng)線程的時(shí)候不會(huì)發(fā)生阻塞,而且還是延遲執(zhí)行的。我們知道,代碼的執(zhí)行速度是非常快的,循環(huán)20次幾乎一瞬間就完成了,這是i就變成了20,但是線程是延遲執(zhí)行的,當(dāng)線程真正去執(zhí)行的時(shí)候,對(duì)應(yīng)的是同一個(gè)i,這時(shí)i是20,所以輸出的都是20。那么該如何輸出每次循環(huán)的值呢?看下面的代碼:
for (int i = 0; i < 20; i++) { // 定義一個(gè)新的變量 int k = i; // 開(kāi)啟線程 Task.Run(() => { Task.Run(() => Console.WriteLine($"this is {i}_{k} ThreadId: {Thread.CurrentThread.ManagedThreadId.ToString("00")}")); }); }
程序運(yùn)行結(jié)果:
這樣每次循環(huán)的時(shí)候,都重新定義變量k,保證每次都是全新的,所以k的值就是每次循環(huán)的值。
四、線程安全
什么是線程安全呢?線程安全:如果你的代碼在進(jìn)程中有多個(gè)線程同時(shí)運(yùn)行這一段,如果每次運(yùn)行的結(jié)果都跟單線程運(yùn)行時(shí)的結(jié)果一致,那么就是線程安全的。
在什么情況下會(huì)出現(xiàn)線程安全的問(wèn)題呢?
一般都是有全局變量/共享變量/靜態(tài)變量/硬盤(pán)文件/數(shù)據(jù)庫(kù)的值,只要多線程訪問(wèn)和修改,就會(huì)出現(xiàn)線程安全的問(wèn)題??聪旅娴拇a:
int syncNum = 0; int AsyncNum = 0; for (int i = 0; i < 10000; i++) { syncNum++; } Console.WriteLine($"syncNum={syncNum}"); //單線程10000 10000 for (int i = 0; i < 10000; i++) { Task.Run(() => { AsyncNum++; }); } Console.WriteLine($"AsyncNum ={AsyncNum}");
程序運(yùn)行結(jié)果:
這就是線程安全造成的問(wèn)題。那么該如何解決這個(gè)問(wèn)題呢?這時(shí)可以使用lock關(guān)鍵字解決。lock關(guān)鍵字定義如下:
private static readonly object Form_Lock = new object();//鎖對(duì)象的標(biāo)準(zhǔn)寫(xiě)法
修改代碼如下:
int syncNum = 0; int AsyncNum = 0; for (int i = 0; i < 10000; i++) { syncNum++; } Console.WriteLine($"syncNum={syncNum}"); for (int i = 0; i < 10000; i++) { Task.Run(() => { lock (Form_Lock) { AsyncNum++; } }); } // 休眠5秒,等待所有線程都執(zhí)行完畢 Thread.Sleep(5000); Console.WriteLine($"AsyncNum ={AsyncNum}");
程序運(yùn)行結(jié)果:
除了使用lock,我們還可以使用數(shù)據(jù)分拆,避免多線程操作同一個(gè)數(shù)據(jù),這樣又安全又高效。
到此這篇關(guān)于C#多線程相關(guān)操作的文章就介紹到這了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
桌面浮動(dòng)窗口(類(lèi)似惡意廣告)的實(shí)現(xiàn)詳解
本篇文章是對(duì)桌面浮動(dòng)窗口的實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06MessageBox的Buttons和三級(jí)聯(lián)動(dòng)效果
這篇文章主要介紹了MessageBox的Buttons和三級(jí)聯(lián)動(dòng)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11C#實(shí)現(xiàn)文件上傳下載Excel文檔示例代碼
這篇文章主要介紹了C#實(shí)現(xiàn)文件上傳下載Excel文檔示例代碼,需要的朋友可以參考下2017-08-08C# DateTime.ToString根據(jù)不同語(yǔ)言生成相應(yīng)的時(shí)間格式
本文分享了一個(gè)按照不同國(guó)家的語(yǔ)言生成相應(yīng)時(shí)間格式的案例,有需要做國(guó)外網(wǎng)站或者多國(guó)語(yǔ)言網(wǎng)站的朋友可以參考一下。2016-03-03Unity3D Shader實(shí)現(xiàn)動(dòng)態(tài)屏幕遮罩
這篇文章主要為大家詳細(xì)介紹了Unity3D Shader實(shí)現(xiàn)動(dòng)態(tài)屏幕遮罩效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02Unity讀取Excel文件轉(zhuǎn)換XML格式文件
這篇文章主要為大家詳細(xì)介紹了Unity讀取Excel文件轉(zhuǎn)換XML格式文件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06