天天看點

建議85:Task中的異常處理

建議85:Task中的異常處理

在任何時候,異常處理都是非常重要的一個環節。多線程與并行程式設計中尤其是這樣。如果不處理這些背景任務中的異常,應用程式将會莫名其妙的退出。處理那些不是主線程(如果是窗體程式,那就是UI主線程)産生的異常,最終的辦法都是将其包裝到主線程上。

在任務并行庫中,如果對任務運作Wait、WaitAny、WaitAll等方法,或者求Result屬性,都能捕獲到AggregateException異常。可以将AggregateException異常看做是任務并行庫程式設計中最上層的異常。在任務中捕獲的異常,最終都應該包裝到AggregateException中。一個任務并行庫異常的簡單處理示例如下:

建議85:Task中的異常處理
static void Main(string[] args)  
{  
    Task t = new Task(() =>
        {  
            throw new Exception("任務并行編碼中産生的未知異常");  
        });  
    t.Start();  
 
    try  
    {  
        //若有Result,可求Result  
        t.Wait();  
    }  
    catch (AggregateException e)  
    {  
        foreach (var item in e.InnerExceptions)  
        {  
            Console.WriteLine("異常類型:{0}{1}來自:{2}{3}異常内容:{4}", item.GetType(), Environment.NewLine,  
 item.Source, Environment.NewLine, item.Message);  
        }  
    }  
    Console.WriteLine("主線程馬上結束");  
    Console.ReadKey();  
}       
建議85:Task中的異常處理

 上面的代碼輸出:

異常類型:System.Exception  

來自:ConsoleApplication3  

異常内容:任務并行編碼中産生的未知異常  

主線程馬上結束

大家也許已經注意到,雖然運作Wait、WaitAny、WaitAll方法,或者求Result屬性能得到任務的異常資訊,但是這會阻滞目前線程。這往往不是我們所希望看到的,豈能為了得到一個異常就故意等待?這時可以考慮任務并行庫中Task類型的一個功能:新起一個後續任務,就可以解決等待的問題:

建議85:Task中的異常處理
static void Main()  
{  
    Task t = new Task(() =>
    {  
        throw new Exception("任務并行編碼中産生的未知異常");  
    });  
    t.Start();  
    Task ttEnd = t.ContinueWith((task) =>
    {  
        foreach (Exception item in task.Exception.InnerExceptions)  
        {  
            Console.WriteLine("異常類型:{0}{1}來自:{2}{3}異常内容:{4}", item.GetType(), Environment.NewLine,  
               item.Source, Environment.NewLine, item.Message);  
        }  
    }, TaskContinuationOptions.OnlyOnFaulted);  
 
    Console.WriteLine("主線程馬上結束");  
    Console.ReadKey();  
}        
建議85:Task中的異常處理

 輸出為:

主線程馬上結束  

異常類型:System.Exception  

來自:ConsoleApplication3  

異常内容:任務并行編碼中産生的未知異常

以上方法解決了主線程等待的問題,但是仔細研究我們會發現,異常處理沒有回到主線程中,它還是線上程池中。在某些場合,比如對于業務邏輯上特定異常的處理,需要采取這種方式,而且我們也鼓勵這種用法。但很明顯,更多時候我們還需要更進一步将異常處理封裝到主線程。

Task沒有提供将任務中的異常包裝到主線程的接口。一個可行的辦法是,仍舊使用類似Wait的方法來達到此目的。在本建議一開始的代碼中,我們對于主工作任務采用Wait的方法,這是不可取的。因為主工作任務也許會持續一段較長的時間,那樣會阻塞調用者,并讓調用者覺得不能忍受。而本建議的第二段代碼中,新任務隻完成了處理異常,這意味着新任務不會延續較長時間,是以,在這個新任務上維持等待對于調用者來說,是可以忍受的。是以,我們可以采用這個方法将異常包裝到主線程中:

建議85:Task中的異常處理
static void Main(string[] args)  
{  
    Task t = new Task(() =>
    {  
        throw new InvalidOperationException("任務并行編碼中産生的未知異常");  
    });  
    t.Start();  
    Task ttEnd = t.ContinueWith((task) =>
    {  
        throw task.Exception;  
    }, TaskContinuationOptions.OnlyOnFaulted);  
    try  
    {  
        tEnd.Wait();  
    }  
    catch (AggregateException err)  
    {  
        foreach (var item in err.InnerExceptions)  
        {  
            Console.WriteLine("異常類型:{0}{1}來自:  
               {2}{3}異常内容:{4}", item.InnerException.GetType(),  
               Environment.NewLine, item.InnerException.Source,  
               Environment.NewLine, item.InnerException.Message);  
        }  
    }  
    Console.WriteLine("主線程馬上結束");  
    Console.ReadKey();  
}       
建議85:Task中的異常處理

 輸出為:

異常類型:System.InvalidOperationException  

來自:ConsoleApplication3  

異常内容:任務并行編碼中産生的未知異常  

主線程馬上結束  

故事并沒有到此結束。 

對線程調用Wait方法(或者求Result)不是最好的辦法,因為它會阻滞主線程,并且CLR在背景會新起線程池線程來完成額外的工作。如果要包裝異常到主線程,另外一個方法就是使用事件通知的方式:

建議85:Task中的異常處理
static event EventHandler<AggregateExceptionArgs> AggregateExceptionCatched;  
 
public class AggregateExceptionArgs: EventArgs  
{  
    public AggregateException AggregateException{ get; set; }  
}  
 
static void Main(string[] args)  
{  
    AggregateExceptionCatched += EventHandler<AggregateExceptionArgs>(Program_AggregateExceptionCatched);  
    Task t = new Task(() =>
    {  
        try  
        {  
            throw new InvalidOperationException("任務并行編碼中産生的未知異常");  
        }  
        catch (Exception err)  
        {  
            AggregateExceptionArgs errArgs = new AggregateExceptionArgs()  
                { AggregateException = new AggregateException(err) };  
            AggregateExceptionCatched(null, errArgs);  
        }  
    });  
    t.Start();  
 
    Console.WriteLine("主線程馬上結束");  
    Console.ReadKey();  
 
}  
 
static void Program_AggregateExceptionCatched(object sender, AggregateExceptionArgs e)  
{  
    foreach (var item in e.AggregateException.InnerExceptions)  
    {  
        Console.WriteLine("異常類型:{0}{1}來自:{2}{3}異常内容:{4}",  
           item.GetType(), Environment.NewLine, item.Source,  
           Environment.NewLine, item.Message);  
    }  
}       
建議85:Task中的異常處理

 在這個例子中,我們聲明了一個委托AggregateExceptionCatchHandler,它接受兩個參數,一個是事件的通知者;另一個是事件變量AggregateExceptionArgs。AggregateExceptionArgs是為了包裝異常而建立的一個類型。在主線程中,我們為事件AggregateExceptionCatched配置設定了事件處理方法Program_AggregateExceptionCatched,當任務Task捕獲到異常時,代碼引發事件。

這種方式完全沒有阻滞主線程。如果是在Winform或WPF窗體程式中,要在事件處理方法中處理UI界面,還可以将異常資訊交給窗體的線程模型去處理。是以,最終建議大家采用事件通知的模型處理Task中的異常。

注意 任務排程器TaskScheduler提供了這樣一個功能,它有一個靜态事件用于處理未捕獲到的異常。一般不建議這樣使用,因為事件回調是在進行垃圾回收的時候才發生的。如下:

建議85:Task中的異常處理
static void Main()  
{  
    TaskScheduler.UnobservedTaskException += new EventHandler<
        UnobservedTaskExceptionEventArgs>(TaskScheduler_UnobservedTaskException);  
    Task t = new Task(() =>
    {  
        throw new Exception("任務并行編碼中産生的未知異常");  
    });  
    t.Start();  
    Console.ReadKey();  
    t.Dispose();  
    t = null;  
    //GC.Collect(0);  
    Console.WriteLine("主線程馬上結束");  
    Console.ReadKey();  
}  
 
static void TaskScheduler_UnobservedTaskException(object sender,  
    UnobservedTaskExceptionEventArgs e)  
{  
    foreach (Exception item in e.Exception.InnerExceptions)  
    {  
        Console.WriteLine("異常類型:{0}{1}來自:{2}{3}異常内容:{4}",  
            item.GetType(), Environment.NewLine, item.Source,  
            Environment.NewLine, item.Message);  
    }  
    //将異常辨別為已經觀察到  
    e.SetObserved();  
}       
建議85:Task中的異常處理

 上面的這段代碼運作的結果中并不會輸出異常資訊,因為發生異常的時刻,并沒有發生垃圾回收(垃圾回收時機由CLR決定)。必須要将GC.Collect(0)的注釋去掉,強制執行垃圾回收,才會觀察到異常資訊。這也正是此種方式的局限性。

轉自:《編寫高品質代碼改善C#程式的157個建議》陸敏技

繼續閱讀