上一篇:異步多執行緒之Parallel
例外處理
小伙伴有沒有想過,多執行緒的例外怎么處理,同步方法內的例外處理,想必都非常非常熟悉了,那多執行緒是什么樣的呢,接著我講解多執行緒的例外處理
首先,我們定義個任務串列,當 11、12 次的時候,拋出一個例外,最外圍使用 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},執行失敗");
}
if (name.ToString().Equals("第 12 次"))
{
throw new Exception($"{t},執行失敗");
}
Console.WriteLine($"{t},執行成功");
};
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();
}
啟動程式,可以看到 vs 捕獲到了例外的代碼行,但 catch 并未捕獲到例外,這是為什么呢?是因為執行緒里面的例外被吞掉了,從運行的結果也可以看到,main end 在子執行緒沒有執行任時就已經結束了,那說明 catch 已經執行過去了,
那有沒有辦法捕獲多執行緒的例外呢?答案:有的,等待執行緒完成計算即可
看下面代碼,有個特殊的地方 AggregateException.InnerExceptions 專門為多執行緒準備的,可以查看多執行緒例外資訊
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},執行失敗");
}
if (name.ToString().Equals("第 12 次"))
{
throw new Exception($"{t},執行失敗");
}
Console.WriteLine($"{t},執行成功");
};
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();
}
啟動執行緒,可以看到任務全部執行完畢,且 AggregateException.InnerExceptions 存盤了,子執行緒執行時的例外資訊

但 WaitAll 不好,總不能一直 WaitAll 吧,它會卡界面,并不適用于異步場景對吧,接著來看另外一直解決方案,就是子執行緒里不允許出現例外,如果有自己處理好,即 try catch 包一下,平時作業中建議這么做,
使用 try catch 將子執行緒執行的代碼包一下,且在 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 =>
{
try
{
Thread.Sleep(2 * 1000);
if (name.ToString().Equals("第 11 次"))
{
throw new Exception($"{t},執行失敗");
}
if (name.ToString().Equals("第 12 次"))
{
throw new Exception($"{t},執行失敗");
}
Console.WriteLine($"{t},執行成功");
}
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();
}
啟動程式,可以看到任務全部執行,且子執行緒例外也捕獲到

執行緒取消
有時候會有這樣的場景,多個任務并發執行,如果某個任務失敗了,通知其他的任務都停下來,首先打個預防針 Task 在外部無法中止的,Thread.Abort 不靠譜,其實執行緒取消的這個想法是錯誤的,執行緒是 OS 的資源,程式是無法掌控什么時候取消,發出一個動作可能立馬取消,也可能等 1 s 取消,
解決方案:執行緒自己停止自己,定義公共的變數,修改變數狀態,其他執行緒不斷檢測公共變數
例如:CancellationTokenSource 就是公共變數,初始化為 false 狀態,程式執行 CancellationTokenSource .Cancel() 方法會取消,其他執行緒檢測到 CancellationTokenSource .IsCancellationRequested 會是取消狀態,CancellationTokenSource.Token 在啟動 Task 時傳入,如果已經 CancellationTokenSource.Cancel() ,這個任務會放棄啟動,拋出一個例外的形式放棄,
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},執行失敗");
}
if (name.ToString().Equals("第 12 次"))
{
throw new Exception($"{t},執行失敗");
}
if (cancellationTokenSource.IsCancellationRequested) // 檢測信號量
{
Console.WriteLine($"{t},放棄執行");
return;
}
Console.WriteLine($"{t},執行成功");
}
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();
}
啟動程式,可以看到 11、12 此任務失敗,18、19 放棄了任務執,有的小伙伴疑問了,12 之后的部分為什么執行成功了,因為 CPU 是分時分片的嗎,會有延遲,延遲少不了,

臨時變數
首先看個代碼,回圈 5 次,多執行緒的方式,依次輸出序號
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();
}
啟動程式,不是我們預期的結果 0、1、2、3、4,為什么是 5 個 5 呢?因為全程只有一個 i ,當主執行緒執行完畢時 i = 5 ,但子執行緒可能還沒有開始執行任務,輪到子執行緒取 i 時,已經是主執行緒 1 回圈完畢后的 5 了,

改造代碼:在 for 回圈內加一行代碼 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();
}
啟動程式,可以看到是我們預期的結果 0、1、2、3、4,為什么會這樣子呢?因為全程有 5 個 k,每次回圈都會創建一個 k 存盤當前的 i,不同的子執行緒使用的也是,每次回圈的 i 值,

執行緒安全
首先為什么會有執行緒安全的概念呢?首先我們來看一個正常程式,如下
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();
}
啟動程式,可以看到回圈 10000 次,最終的求和與串列里的資料量都是 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();
}
啟動程式,可以看到,兩個結果都不是 10000 呢?這就是執行緒安全
因為 TotalCount 是個共享的變數,當多個執行緒去取 TotalCount 進行 +1 后,執行緒都去放值的時候,后一個執行緒會替換掉前一個執行緒放置的值,所以就會形成做最終不是 10000 的結果,串列,可以看做是一個連續的塊,當多執行緒添加的時候,也會進行覆寫,

如何解決呢?答案:lock、安全佇列、拆分合并計算,下面對 lock 進行講解,安全佇列與拆分合并計算,有興趣的小伙伴可以私下交流
1 .lock
第一種,通過加鎖的方式,這種也是日常作業總常用的一種,首先定義個私有的靜態參考型別的變數,然后將需要鎖的運算放到 lock () 方法內
在 { } 內同一時刻,只有一個執行緒執行,所以盡可能 {} 放置必要的邏輯運行提高效率,lock 只能鎖參考型別,原理是占用這個參考鏈接,不要用 string 會享元,即如 lock() 是相同的字串,無論定義多少個變數,其實都是一個,
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();
}
}
啟動程式,可以看到,此時在多執行緒的情況下,最終的結果是正常的

這段代碼,是官方推薦寫法 private 防止外面也被參考,static 保證全場唯一
private static readonly object _lock = new object();
擴展:與 lock 等價的有個 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://www.uj5u.com/ruanti/402470.html
標籤:其他
上一篇:Latex寫創新作業
