C#多線程編程(4)--異常處理+前三篇的總結


  本來是打算講並行For和PLINQ的,但是我感覺前三篇我沒有講得很清晰。之前一直在看《CLR via C#》(后文簡稱CLR)的多線程部分,其中有些部分不是很明白,今天翻開《果殼中的C#》(后文簡稱果殼),看了下多線程部分,發現這本書講的內容雖然很少,但是提綱挈領,把我之前讀CLR中的知識點都串了起來。之前講關鍵字async,await時,提到了狀態機。其實,await會被編譯成awaiter.GetAwaiter()方法,以及之后的委托,果殼中有很簡單的例子來講解,讓我茅塞頓開,還有其他的部分也是這樣。因此我決定寫個總結,就是把之前我講的我認為還沒講透的地方換種方式再講一遍,目的是讓大家,也是我自己真正的明白多線程的工作原理以及如何更好的使用異步。

  在總結之前,我要先介紹一下多線程的異常處理。多線程的異常處理分兩種:非池化線程(自己new出來的線程)和池化線程(調用Task)。我們先來看一個非池化的例子。

static void Main(string[] args)
{
    try
    {
        Go();
    }
    catch (NullReferenceException exception)
    {
        //代碼永遠執行不到這里
    }
}
static void Go()
{
    new Thread(() =>{
        Thread.Sleep(1000);
        throw new NullReferenceException();
    }).Start();
}

上述代碼中,程序永遠不會執行到catch里,因為當前try catch只能捕獲到主線程中的異常,無法捕獲其他線程中的異常。處理辦法是將異常處理部分放到Go()函數中。

在池化線程中,任務中拋出的異常都會被捕獲,並收集到AggregateException中,例子如下

static void Main(string[] args)
{
    Go();
    Console.ReadLine();
}
static void Go()
{
    try { Task.Run(() => throw new NullReferenceException()).Wait(); }
    catch (AggregateException ex){
        if (ex.InnerException is NullReferenceException)
            Console.WriteLine("捕獲異常");
    }
}

若你用的是vs2013或者更低版本,當運行這段代碼時,會彈出異常提示,再次點擊運行,就能看到“捕獲異常”,vs2015和vs2017則不需要,這是因為VS將遇到的異常彈出,是為了方便調試。這個例子可以看到任務中拋出了NullReferenceException異常,並在catch塊中捕獲了該異常。可以注意到異常的類型是AggregateException,當任務中拋出了多個異常,會存放在InnerExceptions中,這個和InnerException不同,這是一個只讀集合,里面存放的是全部的異常。上述代碼中,若不顯示調用wait()方法,則不會捕獲到異常。只有等待任務,或者嘗試獲取任務的返回值時,線程池才會拋出異常列表中的第一個異常。

接下來是總結,說是總結,其實是將前面未講透的知識點仔細講解一下。

先說一下async和await關鍵字。

static void Main(string[] args){
    GoAsync();
    Console.WriteLine("異步運行");
    Console.ReadLine();
}
static async void GoAsync(){    
    await Task.Run(() => {
        //模擬其他任務
        Thread.Sleep(2000); });
    Console.WriteLine("任務結束");
}

可以看到程序運行了GoAsync()后,直接打印出了“異步運行”四個字,然后過了大約2秒才打印出任務結束。這表明GoAsync方法為異步方法,它不會阻塞線程,就是程序在執行到該函數后,不需要等待該方法結束,而是直接繼續執行下一行代碼。可以注意到GoAsync()方法標有async,且在Task.Run前添加了await關鍵字,這表明程序會等待Task任務,知道該任務結束后,才會繼續執行。其實,這段代碼會被編譯器翻譯成大概下面代碼的樣子(我省略了絕大部分代碼,只保留關鍵的部分,若有興趣,可以將該段代碼編譯后,調用IL反編譯器,查看編譯器真正的編譯結果)。

static void Main(string[] args){
    GoAsync();
    Console.WriteLine("異步運行");
    Console.ReadLine();
}
static void GoAsync(){            
    var awaiter = Task.Run(() => {
        //模擬其他任務
        Thread.Sleep(2000); }).GetAwaiter();
    awaiter.OnCompleted(() => Console.WriteLine("任務結束"));
}

調用await關鍵字,相當於在此處獲取該任務的awaiter,該awaiter在任務結束后,會調用傳入到OnCompleted方法中的委托。可以看到上述的寫法沒有async和await關鍵字優美,且沒有辦法標識GoAsync()方法為異步,只能以名字區分。async關鍵字能夠很清晰的表明該方法是異步方法,且await用法簡單,只要放在想要等待的任務前面就可以了,編譯器會把await關鍵后面的部分放入到awaiter.OnCompleted()里面,等到任務結束后再開始執行。

下面來介紹下TaskCompletionSource,該類型是用來實現線程的返回值問題的。TaskCompleteSource的結構大概是這樣的:

public class TaskCompletionSource<TResult>{
    public void SetResult(TResult result);
    public void SetException(Exception ex);
    public void SetCancel();
    public bool TrySetException(Exception ex);
    ...
}

每個Set方法都只能調用一次,再次調用會拋出異常,而Try方法會返回false。

下面就利用TaskCompletionSource來實現我們自己的Run()方法:

static void Main(string[] args)
{
    GoAsync();
    Console.WriteLine("異步運行");
    Console.ReadLine();
}
static async void GoAsync()
{
    var t = await Run(() =>
    {
        //模擬其他任務
        Thread.Sleep(2000);
        return "任務完成";
    });
    Console.WriteLine(t);
}
//自己的Run方法
static Task<TResult> Run<TResult>(Func<TResult> func)
{
    var tcs = new TaskCompletionSource<TResult>();
    ThreadPool.QueueUserWorkItem(t =>
    {
        try
        {
            tcs.SetResult(func());
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    }, null);
    return tcs.Task;
}

可以看到,Run()方法中,通過tcs.setResult()方法,成功的將返回值拋了出來,並返回了含有結果的Task。該段代碼和上面的調用Task.Run的GoAsync()方法一樣。也可以使用TaskCompletionSource加上定時器來實現Task.Delay()方法,而不用顯式調用線程。該方法的實現就留給讀者自行完成。

以上,本文介紹了多線程中異常的捕獲和處理,其中分為非池化線程和池化線程兩種,其中非池化的異常處理要放在待執行的方法中,而池化線程可以通過調用Result或者await來將異常統一存放到AggregateException中統一處理。然后我針對前面三篇文章中沒有講透的點重新講解了一下。包括await關鍵字的機制,編譯器是通過Awaiter.OnComplete來實現的。之后是TaskCompletionSource,該類型是用來實現線程的返回值問題的,也講解了如何實現自己的Task.Run()方法。並給讀者留了一個用TaskCompletionSource和定時器來實現Task.Delay()的練手題。

歡迎大家在我的評論區與我交流。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM