Task類在.NET4.5中,做了一些改進,比如新增了方法ConfigureAwait,Delay,Run等方法。其中一個重要修改,就是對于異常的處理。
在.NET4.0中,Task中抛出的異常,如果沒有去捕獲,在Task被垃圾回收的時候,析構函數檢測到該Task對象還有未被處理過的異常,會抛出這個異常,并且導緻程序終結,程序終結的時間是由垃圾回收器和析構方法決定的。(可以通過注冊TaskSchedular.UnobservedTaskException事件處理未捕獲的異常。)
ThrowUnobservedTaskExceptions節點
在.NET4.5中,微軟改變了政策,對于task中未處理的異常,預設情況下不會導緻殺死程序。這個可以通過配置ThrowUnobservedTaskExceptions節點實作。
預設情況下,ThrowUnobservedTaskExceptions這個節點的enabled=false。如下配置。
<configuration>
<runtime>
<ThrowUnobservedTaskExceptions enabled="false"/>
</runtime>
</configuration>
這種情況下,在.NET4.5中,GC回收對象的時候,是不會導緻程式崩潰的,未捕獲的異常就這樣消失了。比如下面的代碼:
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
var t = Task.Factory.StartNew<int>(() => { throw new Exception("xxxxxx"); return 1; }
, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
}
while (true)
{
GC.Collect();
Thread.Sleep(1000);
}
Console.ReadKey();
}
但是如果設定<ThrowUnobservedTaskExceptions enabled="true"/>,那麼程式就會崩潰,這和在.NET4.0中的結局一樣。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiInBnaucTN5UFZitkUwd3RNFUQ0cGbFF1RyJXMQFDTvl2S39CX5AzLcdDNvwFMw00LcJDMzZWe39CXt92Yu8GdjFTNuMzcvw1LcpDc0RHaiojIsJye.jpg)
當然,對于所有的異常,都是建議捕獲并且處理的。.NET4.0和4.5都提供了TaskScheduler.UnobservedTaskException事件,通過監聽這個事件,也可以捕獲到這個異常。
代碼如下:
TaskScheduler.UnobservedTaskException += (o, ev) =>
{
Console.WriteLine(ev.Exception);
ev.SetObserved();
Console.WriteLine("---------");
};
注意:ev.SetObserved();方法必須要調用,這樣才能阻止程序崩潰。
.NET4.0中Task的異常
還有一處改進是對于異常的捕獲上。對于Task中抛出的異常,外部的try catch是無法捕獲的。例如:
static Task<int> f()
{
try
{
var t = Task.Factory.StartNew<int>(() => { throw new Exception("xxxxxx"); return 1; });
return t;
}
catch
{
Console.WriteLine("error");
}
return null;
}
無論是在.net4.0還是.net4.5,上述代碼都是不會走到catch中的。隻有當運作Task.Wait或者讀取Task.Result的時候(這些方法都會引起阻塞),才會抛出異常,由于運作的Task可能是包含了多個子Task,或者在WaitAll多個Task,那麼異常可能會出現多個。Task類會把這些異常包成AggregateException異常。要獲得異常的正真資訊,需要通路AggregateException.InnerExceptions屬性。如下代碼:
Task<int> t1 = Task.Factory.StartNew<int>(() =>
{
throw new Exception("error1");
});
Task<int> t2 = Task.Factory.StartNew<int>(() =>
{
throw new Exception("error2");
});
Task<int> t3 = Task.Factory.StartNew<int>(() =>
{
throw new Exception("error3");
});
try
{
Task.WaitAll(t1,t2,t3);
}
catch (AggregateException ex)
{
Console.WriteLine("Exception Type:{0}", ex.GetType());
Console.WriteLine("Exception Message:{0}", ex.Message);
Console.WriteLine("Exception StackTrace:{0}", ex.StackTrace);
Console.WriteLine("Exception InnerException.Message:{0}", ex.InnerException.Message);
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine("InnerExceptions.Message:{0}", innerEx.Message);
}
}
Task.WaitAll三個Task,捕獲異常的時候,類型為AggregateException,并且可以通過InnerExceptions,周遊出每一個異常。這裡值得注意的是,如果AggregateException的InnerExceptions有3個異常的話,AggregateException的InnerException會抛出哪個異常?根據老趙的說法,C#開發團隊“故意”不提供文檔說明究竟會抛出哪個異常。因為他們并不想做出這方面的限制,因為這部分行為一旦寫入文檔,便成為一個規定和限制,為了類庫的相容性今後也無法對此做出修改。
上面代碼的輸出如下:
async/await
在.net 4.5中,c#5.0的文法支援了async/await的寫法,這種寫法下,可以按照同步的思路寫異步方法。而且,異常處理也變得可以直接捕獲,在await的時候,代碼可以直接捕獲異常,例如下面的代碼。
static void Main(string[] args)
{
var t = f();
Console.ReadKey();
}
async static Task<int> f()
{
int val = 0;
try
{
val = await Task.Factory.StartNew<int>(() => { throw new NotSupportedException("xxxxxx"); return 1; });
}
catch (Exception ex)
{
Console.WriteLine("Exception Type:{0}", ex.GetType());
Console.WriteLine("Exception Message:{0}", ex.Message);
Console.WriteLine("Exception StackTrace:{0}", ex.StackTrace);
}
return val;
}
輸出如下:
注意,可以看到錯誤的堆棧資訊。從System.Threading.Tasks.Task類切換到了 System.Runtime.CompilerServices.TaskAwaiter。
C# 使用了SynchronizationContext類完成了這個切換。當await一個Task的時候,目前的 SynchronizationContext對象被存儲下來。當方法繼續向下運作的時候,await關鍵字的結構使用Post方法,在之前儲存的SynchronizationContext類的基礎上,繼續運作該方法。更多細節不在這裡描述。是以隻要await關鍵字的方法上出現了異常,就可以捕獲掉,并且捕獲的異常類型,就可以不把異常往上層抛,還有一個不同點是await方法捕獲的異常類型就是直接的類型,而不是System.AggregateException。
但是如果在await等待是多個Task,并且這多個Task都抛出了異常,那麼,最終捕獲的異常也會是System.AggregateException類型。并且也可以周遊出所有的異常。和之前的處理同步情況下是一樣的。
static void Main(string[] args)
{
var t = f();
Console.WriteLine(t.Result);
Console.ReadKey();
}
async static Task<int> f()
{
int val = 0;
Task<int[]> all = null;
try
{
Task<int> t1 = Task.Factory.StartNew<int>(() =>
{
throw new NotImplementedException("error1");
return 1;
});
Task<int> t2 = Task.Factory.StartNew<int>(() =>
{
throw new NotImplementedException("error2");
return 2;
});
Task<int> t3 = Task.Factory.StartNew<int>(() =>
{
throw new NotImplementedException("error3");
return 3;
});
await (all = Task.WhenAll(t1, t2, t3));
val = all.Result.Sum();
}
catch (Exception ex)
{
Console.WriteLine("Exception Type:{0}", ex.GetType());
Console.WriteLine("Exception Message:{0}", ex.Message);
Console.WriteLine("Exception StackTrace:{0}", ex.StackTrace);
foreach (var innerEx in all.Exception.InnerExceptions)
{
Console.WriteLine("InnerExceptions.Message:{0}", innerEx.Message);
}
}
return val;
}
上述代碼中,使用了WhenAll方法,傳回的是一個Task類。由于t1,t2,t3中都包含了異常,是以傳回的Task中有3個異常,但await關鍵字隻會允許抛出一個具體的異常,是以,此處抛出了第一個異常。通過對all變量的周遊,可以得到所有的異常。
WhenAll 方法
WhenAll方法是.NET4.5中新增的方法。它的傳回值是一個Task,僅當所有的Task都完成的時候,傳回的這個Task才算完成,并且傳回值可以是各個task傳回值的一個數組。
是以可以把WhenAll方法看成是一組Task的合并。WhenAll和WaitAll有點類似,但本質上很多不同。
1.調用Task.WaitAll的時候,會阻塞目前線程,直到所有的Task都完成了。而Task.WhenAll方法不會阻塞目前線程,而是直接傳回了一個Task,隻有在讀取這個Task的Result的時候,才會引起阻塞。
2.WaitAll的各類重載方法,它們的傳回值是void或者bool,而WhenAll的傳回值是Task。是以WhenAll方法更好的支援async/await異步寫法。
下面代碼示範WhenAll方法:
static void Main(string[] args)
{
Task<int> t1 = Task.Factory.StartNew<int>(() =>
{
Thread.Sleep(1000);
return 1;
});
Task<int> t2 = Task.Factory.StartNew<int>(() =>
{
Thread.Sleep(2000);
return 2;
});
Task<int> t3 = Task.Factory.StartNew<int>(() =>
{
Thread.Sleep(3000);
return 3;
});
var all = Task.WhenAll(t1, t2, t3);
while (true)
{
Console.WriteLine("{0} IsCompleted:{1}", DateTime.Now.ToString("HH:mm:ss fff"), all.IsCompleted);
Thread.Sleep(200);
if (all.IsCompleted)
break;
}
var c = all.Result;
Console.WriteLine(c.Sum());
}
輸出的結果為:
ConfigureAwait方法
ConfigureAwait方法的作用是指定代碼在執行await操作的時候,是否捕獲上下文,捕獲上下文會帶來性能開銷。不捕獲上下文,在ASP.NET或者GUI程式時,可能帶來問題。更多的細節可以參考Stephen Cleary 的異步程式設計中的最佳做法
總之,.NET4.5中的Task的一些改進,都是為了迎合異步async/await的寫法而做出的改進。更多參考資料: