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();
}
}
}程序運行結果:

那么在多線程中如何捕獲異常呢?是不是也可以使用try-catch進行捕獲?我們先看下面的代碼:
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}";
// 開啟線程
Task.Run(() =>
{
Console.WriteLine($"{str} 開始了");
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} 結束了");
});
}
}
catch (Exception ex)
{
Console.WriteLine($"message:{ex.Message}");
}
#endregion
Console.ReadKey();
}
}
}程序運行結果:

我們看到結果中并沒有輸出異常信息,是不是沒有拋出異常呢?我們起代碼進行調試,看調試信息:

我們看到程序中確實也拋出了異常,但是程序卻沒有捕獲到,那么異常去哪里了呢?異常被多線程給吞掉了,那么如何在多線程中捕獲異常呢?如果把try-catch寫在線程里面呢?每一個線程都是單線程的,把try-catch寫在每一個線程里面就沒有意義了。在多線程中捕獲異常,需要使用到WaitAll(),看下面的代碼:
try
{
// 定義一個Task類型的List集合
List<Task> taskList = new List<Task>();
for (int i = 0; i < 30; i++)
{
string str = $"main_{i}";
// 開啟線程,并把線程添加到集合中
taskList.Add(Task.Run(() =>
{
Console.WriteLine($"{str} 開始了");
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} 結束了");
}));
}
// 等待所有線程都執(zhí)行完
Task.WaitAll(taskList.ToArray());
}
catch (Exception ex)
{
Console.WriteLine($"message:{ex.Message}");
}我們用代碼進行調試,調試結果:

這時就可以進入到catch里面了,我們監(jiān)視ex,發(fā)現ex是AggregateException類型的異常,我們在進一步優(yōu)化代碼:
try
{
// 定義一個Task類型的List集合
List<Task> taskList = new List<Task>();
for (int i = 0; i < 30; i++)
{
string str = $"main_{i}";
// 開啟線程,并把線程添加到集合中
taskList.Add(Task.Run(() =>
{
Console.WriteLine($"{str} 開始了");
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} 結束了");
}));
}
// 等待所有線程都執(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}");
}最后運行程序:

我們發(fā)現這時就可以捕獲到具體的異常信息了。
二、線程取消
在上面的示例中,我們捕獲到了多線程中發(fā)生的異常,并且也輸出了異常信息,但是這樣是不友好的。在實際開發(fā)中,我們使用多線程并發(fā)執(zhí)行任務,假如其中某一個任務失敗了或者發(fā)生了異常,我們希望可以通知其他的線程,都停止下來,那么該如何做呢?這時就需要使用到線程取消。
Task不能外部終止任務,只能自己終止自己。
.Net框架提供了CancellationTokenSource類,該類里面有一個bool類型的屬性:IsCancellationRequested,默認是false,表示是否取消線程。還提供了一個Cancel()方法,該方法可以把IsCancellationRequested的屬性值設置為true,并且不能在設置回去。代碼如下:
// 實例化對象
CancellationTokenSource cts = new CancellationTokenSource();
for (int i = 0; i < 20; i++)
{
string str = $"main_{i}";
// 開啟線程
Task.Run(() =>
{
try
{
Console.WriteLine($"{str} 開始了");
// 暫停
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} 結束了");
}
else
{
Console.WriteLine($"{str} 線程取消");
}
}
catch (Exception ex)
{
// 發(fā)生了異常,將IsCancellationRequested的值設置為true
cts.Cancel();
Console.WriteLine($"message:{ex.Message}");
}
});
}程序運行結果:

可以看到,當有異常發(fā)生之后,有的線程就被取消了。這樣就初步實現了線程取消。
在上面的示例中,我們是先開啟了線程,如果發(fā)生了異常,則取消線程。那么會有這樣一種情況:線程中發(fā)生了異常,可能這時候有的線程還沒有開啟,那么能不能就不讓這些線程在開啟呢?Task的Run方法有一個重載:

第二個參數就表示取消線程。而且CancellationTokenSource類里面正好有這個參數:

所以,我們可以利用Run方法的重載來實現不開啟線程,代碼如下:
try
{
// 實例化對象
CancellationTokenSource cts = new CancellationTokenSource();
// 創(chuàng)建Task類型的集合
List<Task> taskList = new List<Task>();
for (int i = 0; i < 20; i++)
{
string str = $"main_{i}";
// 開啟線程 Task.run 以后 添加Token 就可以在某一個線程發(fā)生異常之后,讓沒有開啟的線程不開啟了
taskList.Add(Task.Run(() =>
{
try
{
Console.WriteLine($"{str} 開始了");
// 暫停
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} 結束了");
}
else
{
Console.WriteLine($"{str} 線程取消");
}
}
catch (Exception ex)
{
// 發(fā)生了異常,將IsCancellationRequested的值設置為true
cts.Cancel();
}
}, cts.Token));
}
// 等待所有線程執(zhí)行完
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException are)
{
foreach (var exception in are.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
}程序運行結果:

輸出結果中有一句話:已取消一個任務,但是我們的代碼里面沒有打印這句話,這是從哪里來的呢?這是因為第二個參數Token的原因,加了這個參數以后,如果就線程發(fā)生了異常,就不在繼續(xù)開啟線程。
三、臨時變量
我們先來看看下面一段代碼:
for (int i = 0; i < 20; i++)
{
// 開啟線程
Task.Run(() =>
{
Task.Run(() => Console.WriteLine($"this is {i} ThreadId: {Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
});
}這段代碼的輸出結果是什么呢?我們運行程序查看結果:

可能有人會感到疑惑:為什么輸出的都是20呢,而不是每次循環(huán)變量的值?這是什么原因呢。這是因為我們申請線程的時候不會發(fā)生阻塞,而且還是延遲執(zhí)行的。我們知道,代碼的執(zhí)行速度是非常快的,循環(huán)20次幾乎一瞬間就完成了,這是i就變成了20,但是線程是延遲執(zhí)行的,當線程真正去執(zhí)行的時候,對應的是同一個i,這時i是20,所以輸出的都是20。那么該如何輸出每次循環(huán)的值呢?看下面的代碼:
for (int i = 0; i < 20; i++)
{
// 定義一個新的變量
int k = i;
// 開啟線程
Task.Run(() =>
{
Task.Run(() => Console.WriteLine($"this is {i}_{k} ThreadId: {Thread.CurrentThread.ManagedThreadId.ToString("00")}"));
});
}程序運行結果:

這樣每次循環(huán)的時候,都重新定義變量k,保證每次都是全新的,所以k的值就是每次循環(huán)的值。
四、線程安全
什么是線程安全呢?線程安全:如果你的代碼在進程中有多個線程同時運行這一段,如果每次運行的結果都跟單線程運行時的結果一致,那么就是線程安全的。
在什么情況下會出現線程安全的問題呢?
一般都是有全局變量/共享變量/靜態(tài)變量/硬盤文件/數據庫的值,只要多線程訪問和修改,就會出現線程安全的問題??聪旅娴拇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}");程序運行結果:

這就是線程安全造成的問題。那么該如何解決這個問題呢?這時可以使用lock關鍵字解決。lock關鍵字定義如下:
private static readonly object Form_Lock = new object();//鎖對象的標準寫法
修改代碼如下:
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}");程序運行結果:

除了使用lock,我們還可以使用數據分拆,避免多線程操作同一個數據,這樣又安全又高效。
到此這篇關于C#多線程相關操作的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
C# DateTime.ToString根據不同語言生成相應的時間格式
本文分享了一個按照不同國家的語言生成相應時間格式的案例,有需要做國外網站或者多國語言網站的朋友可以參考一下。2016-03-03

