建議85:Task中的異常處理
在任何時候,異常處理都是非常重要的一個環節。多線程與并行程式設計中尤其是這樣。如果不處理這些背景任務中的異常,應用程式将會莫名其妙的退出。處理那些不是主線程(如果是窗體程式,那就是UI主線程)産生的異常,最終的辦法都是将其包裝到主線程上。
在任務并行庫中,如果對任務運作Wait、WaitAny、WaitAll等方法,或者求Result屬性,都能捕獲到AggregateException異常。可以将AggregateException異常看做是任務并行庫程式設計中最上層的異常。在任務中捕獲的異常,最終都應該包裝到AggregateException中。一個任務并行庫異常的簡單處理示例如下:
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();
}
上面的代碼輸出:
異常類型:System.Exception
來自:ConsoleApplication3
異常内容:任務并行編碼中産生的未知異常
主線程馬上結束
大家也許已經注意到,雖然運作Wait、WaitAny、WaitAll方法,或者求Result屬性能得到任務的異常資訊,但是這會阻滞目前線程。這往往不是我們所希望看到的,豈能為了得到一個異常就故意等待?這時可以考慮任務并行庫中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();
}
輸出為:
主線程馬上結束
異常類型:System.Exception
來自:ConsoleApplication3
異常内容:任務并行編碼中産生的未知異常
以上方法解決了主線程等待的問題,但是仔細研究我們會發現,異常處理沒有回到主線程中,它還是線上程池中。在某些場合,比如對于業務邏輯上特定異常的處理,需要采取這種方式,而且我們也鼓勵這種用法。但很明顯,更多時候我們還需要更進一步将異常處理封裝到主線程。
Task沒有提供将任務中的異常包裝到主線程的接口。一個可行的辦法是,仍舊使用類似Wait的方法來達到此目的。在本建議一開始的代碼中,我們對于主工作任務采用Wait的方法,這是不可取的。因為主工作任務也許會持續一段較長的時間,那樣會阻塞調用者,并讓調用者覺得不能忍受。而本建議的第二段代碼中,新任務隻完成了處理異常,這意味着新任務不會延續較長時間,是以,在這個新任務上維持等待對于調用者來說,是可以忍受的。是以,我們可以采用這個方法将異常包裝到主線程中:
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();
}
輸出為:
異常類型:System.InvalidOperationException
來自:ConsoleApplication3
異常内容:任務并行編碼中産生的未知異常
主線程馬上結束
故事并沒有到此結束。
對線程調用Wait方法(或者求Result)不是最好的辦法,因為它會阻滞主線程,并且CLR在背景會新起線程池線程來完成額外的工作。如果要包裝異常到主線程,另外一個方法就是使用事件通知的方式:
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);
}
}
在這個例子中,我們聲明了一個委托AggregateExceptionCatchHandler,它接受兩個參數,一個是事件的通知者;另一個是事件變量AggregateExceptionArgs。AggregateExceptionArgs是為了包裝異常而建立的一個類型。在主線程中,我們為事件AggregateExceptionCatched配置設定了事件處理方法Program_AggregateExceptionCatched,當任務Task捕獲到異常時,代碼引發事件。
這種方式完全沒有阻滞主線程。如果是在Winform或WPF窗體程式中,要在事件處理方法中處理UI界面,還可以将異常資訊交給窗體的線程模型去處理。是以,最終建議大家采用事件通知的模型處理Task中的異常。
注意 任務排程器TaskScheduler提供了這樣一個功能,它有一個靜态事件用于處理未捕獲到的異常。一般不建議這樣使用,因為事件回調是在進行垃圾回收的時候才發生的。如下:
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();
}
上面的這段代碼運作的結果中并不會輸出異常資訊,因為發生異常的時刻,并沒有發生垃圾回收(垃圾回收時機由CLR決定)。必須要将GC.Collect(0)的注釋去掉,強制執行垃圾回收,才會觀察到異常資訊。這也正是此種方式的局限性。
轉自:《編寫高品質代碼改善C#程式的157個建議》陸敏技