網(wǎng)站首頁 編程語言 正文
異常處理
小伙伴有沒有想過,多線程的異常怎么處理,同步方法內(nèi)的異常處理,想必都非常非常熟悉了。那多線程是什么樣的呢,接著我講解多線程的異常處理
首先,我們定義個(gè)任務(wù)列表,當(dāng) 11、12 次的時(shí)候,拋出一個(gè)異常,最外圍使用 try catch 包一下
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); try { TaskFactory taskFactory = new TaskFactory(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 20; i++) { string name = $"第 {i} 次"; Action<object> action = t => { Thread.Sleep(2 * 1000); if (name.ToString().Equals("第 11 次")) { throw new Exception($"{t},執(zhí)行失敗"); } if (name.ToString().Equals("第 12 次")) { throw new Exception($"{t},執(zhí)行失敗"); } Console.WriteLine($"{t},執(zhí)行成功"); }; tasks.Add(taskFactory.StartNew(action, name)); } } catch (AggregateException aex) { foreach (var item in aex.InnerExceptions) { Console.WriteLine("Main AggregateException:" + item.Message); } } catch (Exception ex) { Console.WriteLine("Main Exception:" + ex.Message); } Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)程序,可以看到 vs 捕獲到了異常的代碼行,但 catch 并未捕獲到異常,這是為什么呢?是因?yàn)榫€程里面的異常被吞掉了,從運(yùn)行的結(jié)果也可以看到,main end 在子線程沒有執(zhí)行任時(shí)就已經(jīng)結(jié)束了,那說明 catch 已經(jīng)執(zhí)行過去了。
那有沒有辦法捕獲多線程的異常呢?答案:有的,等待線程完成計(jì)算即可
看下面代碼,有個(gè)特殊的地方 AggregateException.InnerExceptions 專門為多線程準(zhǔn)備的,可以查看多線程異常信息
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); try { TaskFactory taskFactory = new TaskFactory(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 20; i++) { string name = $"第 {i} 次"; Action<object> action = t => { Thread.Sleep(2 * 1000); if (name.ToString().Equals("第 11 次")) { throw new Exception($"{t},執(zhí)行失敗"); } if (name.ToString().Equals("第 12 次")) { throw new Exception($"{t},執(zhí)行失敗"); } Console.WriteLine($"{t},執(zhí)行成功"); }; tasks.Add(taskFactory.StartNew(action, name)); } Task.WaitAll(tasks.ToArray()); } catch (AggregateException aex) { foreach (var item in aex.InnerExceptions) { Console.WriteLine("Main AggregateException:" + item.Message); } } catch (Exception ex) { Console.WriteLine("Main Exception:" + ex.Message); } Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)線程,可以看到任務(wù)全部執(zhí)行完畢,且 AggregateException.InnerExceptions 存儲(chǔ)了,子線程執(zhí)行時(shí)的異常信息
但 WaitAll 不好,總不能一直 WaitAll 吧,它會(huì)卡界面。并不適用于異步場(chǎng)景對(duì)吧,接著來看另外一直解決方案。就是子線程里不允許出現(xiàn)異常,如果有自己處理好,即 try catch 包一下,平時(shí)工作中建議這么做。
使用 try catch 將子線程執(zhí)行的代碼包一下,且在 catch 打印錯(cuò)誤信息
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); try { TaskFactory taskFactory = new TaskFactory(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 20; i++) { string name = $"第 {i} 次"; Action<object> action = t => { try { Thread.Sleep(2 * 1000); if (name.ToString().Equals("第 11 次")) { throw new Exception($"{t},執(zhí)行失敗"); } if (name.ToString().Equals("第 12 次")) { throw new Exception($"{t},執(zhí)行失敗"); } Console.WriteLine($"{t},執(zhí)行成功"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }; tasks.Add(taskFactory.StartNew(action, name)); } } catch (AggregateException aex) { foreach (var item in aex.InnerExceptions) { Console.WriteLine("Main AggregateException:" + item.Message); } } catch (Exception ex) { Console.WriteLine("Main Exception:" + ex.Message); } Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)程序,可以看到任務(wù)全部執(zhí)行,且子線程異常也捕獲到
線程取消
有時(shí)候會(huì)有這樣的場(chǎng)景,多個(gè)任務(wù)并發(fā)執(zhí)行,如果某個(gè)任務(wù)失敗了,通知其他的任務(wù)都停下來。首先打個(gè)預(yù)防針 Task 在外部無法中止的,Thread.Abort 不靠譜。其實(shí)線程取消的這個(gè)想法是錯(cuò)誤的,線程是 OS 的資源,程序是無法掌控什么時(shí)候取消,發(fā)出一個(gè)動(dòng)作可能立馬取消,也可能等 1 s 取消。
解決方案:線程自己停止自己,定義公共的變量,修改變量狀態(tài),其他線程不斷檢測(cè)公共變量
例如:CancellationTokenSource 就是公共變量,初始化為 false 狀態(tài),程序執(zhí)行 CancellationTokenSource .Cancel() 方法會(huì)取消,其他線程檢測(cè)到 CancellationTokenSource .IsCancellationRequested 會(huì)是取消狀態(tài)。CancellationTokenSource.Token 在啟動(dòng) Task 時(shí)傳入,如果已經(jīng) CancellationTokenSource.Cancel() ,這個(gè)任務(wù)會(huì)放棄啟動(dòng),拋出一個(gè)異常的形式放棄。
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); try { TaskFactory taskFactory = new TaskFactory(); List<Task> tasks = new List<Task>(); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); // bool for (int i = 0; i < 20; i++) { string name = $"第 {i} 次"; Action<object> action = t => { try { Thread.Sleep(2 * 1000); if (name.ToString().Equals("第 11 次")) { throw new Exception($"{t},執(zhí)行失敗"); } if (name.ToString().Equals("第 12 次")) { throw new Exception($"{t},執(zhí)行失敗"); } if (cancellationTokenSource.IsCancellationRequested) // 檢測(cè)信號(hào)量 { Console.WriteLine($"{t},放棄執(zhí)行"); return; } Console.WriteLine($"{t},執(zhí)行成功"); } catch (Exception ex) { cancellationTokenSource.Cancel(); Console.WriteLine(ex.Message); } }; tasks.Add(taskFactory.StartNew(action, name,cancellationTokenSource.Token)); } Task.WaitAll(tasks.ToArray()); } catch (AggregateException aex) { foreach (var item in aex.InnerExceptions) { Console.WriteLine("Main AggregateException:" + item.Message); } } catch (Exception ex) { Console.WriteLine("Main Exception:" + ex.Message); } Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)程序,可以看到 11、12 此任務(wù)失敗,18、19 放棄了任務(wù)執(zhí)。有的小伙伴疑問了,12 之后的部分為什么執(zhí)行成功了,因?yàn)?CPU 是分時(shí)分片的嗎,會(huì)有延遲,延遲少不了。
臨時(shí)變量
首先看個(gè)代碼,循環(huán) 5 次,多線程的方式,依次輸出序號(hào)
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); for (int i = 0; i < 5; i++) { Task.Run(() => { Console.WriteLine(i); }); } Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)程序,不是我們預(yù)期的結(jié)果 0、1、2、3、4,為什么是 5 個(gè) 5 呢?因?yàn)槿讨挥幸粋€(gè) i ,當(dāng)主線程執(zhí)行完畢時(shí) i = 5 ,但子線程可能還沒有開始執(zhí)行任務(wù),輪到子線程取 i 時(shí),已經(jīng)是主線程 1 循環(huán)完畢后的 5 了。
改造代碼:在 for 循環(huán)內(nèi)加一行代碼 int k = i,且在子線程用的變量也改為 k
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); for (int i = 0; i < 5; i++) { int k = i; Task.Run(() => { Console.WriteLine($"k={k},i={i}"); }); } Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)程序,可以看到是我們預(yù)期的結(jié)果 0、1、2、3、4,為什么會(huì)這樣子呢?因?yàn)槿逃?5 個(gè) k,每次循環(huán)都會(huì)創(chuàng)建一個(gè) k 存儲(chǔ)當(dāng)前的 i,不同的子線程使用的也是,每次循環(huán)的 i 值。
線程安全
首先為什么會(huì)有線程安全的概念呢?首先我們來看一個(gè)正常程序,如下
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); int TotalCount = 0; List<int> vs = new List<int>(); for (int i = 0; i < 10000; i++) { TotalCount += 1; vs.Add(i); } Console.WriteLine(TotalCount); Console.WriteLine(vs.Count); Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)程序,可以看到循環(huán) 10000 次,最終的求和與列表里的數(shù)據(jù)量都是 10000,這是正常的
接著,將求和與添加列表,換成多線程,等待全部線程完成工作后,打印信息
static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); int TotalCount = 0; List<int> vs = new List<int>(); TaskFactory taskFactory = new TaskFactory(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 10000; i++) { int k = i; tasks.Add(taskFactory.StartNew(() => { TotalCount += 1; vs.Add(i); })); } Task.WaitAll(tasks.ToArray()); Console.WriteLine(TotalCount); Console.WriteLine(vs.Count); Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
啟動(dòng)程序,可以看到,兩個(gè)結(jié)果都不是 10000 呢?這就是線程安全
因?yàn)?TotalCount 是個(gè)共享的變量,當(dāng)多個(gè)線程去取 TotalCount 進(jìn)行 +1 后,線程都去放值的時(shí)候,后一個(gè)線程會(huì)替換掉前一個(gè)線程放置的值,所以就會(huì)形成做最終不是 10000 的結(jié)果。列表,可以看做是一個(gè)連續(xù)的塊,當(dāng)多線程添加的時(shí)候,也會(huì)進(jìn)行覆蓋。
如何解決呢?答案:lock、安全隊(duì)列、拆分合并計(jì)算。下面對(duì) lock 進(jìn)行講解,安全隊(duì)列與拆分合并計(jì)算,有興趣的小伙伴可以私下交流
1 .lock
第一種,通過加鎖的方式,這種也是日常工作總常用的一種。首先定義個(gè)私有的靜態(tài)引用類型的變量,然后將需要鎖的運(yùn)算放到 lock () 方法內(nèi)
在 { } 內(nèi)同一時(shí)刻,只有一個(gè)線程執(zhí)行,所以盡可能 {} 放置必要的邏輯運(yùn)行提高效率。lock 只能鎖引用類型,原理是占用這個(gè)引用鏈接。不要用 string 會(huì)享元,即如 lock() 是相同的字符串,無論定義多少個(gè)變量,其實(shí)都是一個(gè)。
internal class Program { private static readonly object _lock = new object(); static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); int TotalCount = 0; List<int> vs = new List<int>(); TaskFactory taskFactory = new TaskFactory(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 10000; i++) { int k = i; tasks.Add(taskFactory.StartNew(() => { lock (_lock) { TotalCount += 1; vs.Add(i); } })); } Task.WaitAll(tasks.ToArray()); Console.WriteLine(TotalCount); Console.WriteLine(vs.Count); Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); } }
啟動(dòng)程序,可以看到,此時(shí)在多線程的情況下,最終的結(jié)果是正常的
這段代碼,是官方推薦寫法 private 防止外面也被引用,static 保證全場(chǎng)唯一
private static readonly object _lock = new object();
擴(kuò)展:與 lock 等價(jià)的有個(gè) Monitor,用法如下
private static object _lock = new object(); static void Main(string[] args) { Console.WriteLine($"Main Start,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); int TotalCount = 0; List<int> vs = new List<int>(); TaskFactory taskFactory = new TaskFactory(); List<Task> tasks = new List<Task>(); for (int i = 0; i < 10000; i++) { int k = i; tasks.Add(taskFactory.StartNew(() => { Monitor.Enter(_lock); TotalCount += 1; vs.Add(i); Monitor.Exit(_lock); })); } Task.WaitAll(tasks.ToArray()); Console.WriteLine(TotalCount); Console.WriteLine(vs.Count); Console.WriteLine($"Main End,ThreadId:{Thread.CurrentThread.ManagedThreadId},Datetime:{DateTime.Now.ToLongTimeString()}"); Console.ReadLine(); }
原文鏈接:https://blog.csdn.net/weixin_46785144/article/details/121207862
相關(guān)推薦
- 2021-12-12 Android如何監(jiān)測(cè)文件夾內(nèi)容變化詳解_Android
- 2022-04-21 Linux運(yùn)行級(jí)別介紹和root忘記密碼找回方法
- 2022-07-26 go通過channel獲取goroutine的處理結(jié)果
- 2022-04-22 wampserver You don‘t have permission to access / o
- 2022-08-19 查看Linux的核數(shù)和內(nèi)存等相關(guān)系統(tǒng)配置
- 2022-04-29 Go語言中的并發(fā)goroutine底層原理_Golang
- 2022-04-14 Python實(shí)現(xiàn)用戶注冊(cè)登錄程序_python
- 2023-05-23 深入了解React中的合成事件_React
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支