首先,线程内部出现异常,所以首选处理方式是在Task中使用try…catch…把异常处理掉
try…catch…无法直接捕获Task内部异常:
如下写法,无法捕捉Task内部异常。
try
{
var task = new Task(() =>
{
throw new CustomException("task内部报错了");
});
task.Start();
}
catch(Exception e)
{
}
但是我们可以通过调用Task.Wait(), Result方法,ContinueWith捕捉Task内部的异常。
线程异常的捕获方法有阻塞型捕获和异步捕获两种。
阻塞型捕获异常就是:当一个或多个线程启动后,我们就一直等待,等到所有线程执行完成,判断这些线程中是否出现异常,而后再执行后续的代码。
异步型捕获异常就是:一个线程启动后,我们不去等待他执行完成,而知直接执行后续其他代码。当线程出现异常时,会自动返回异常信息,或者在需要时主动去获取线程异常信息。
①Wait(),Result, GetAwaiter().GetResult() (阻塞型捕获)
Task中抛出的异常可以捕获,但是也不是直接捕获,而是由调用Wait()方法或者访问Result属性的时候获得异常,优先以AggregateException类型抛出异常,如果没有AggregateException异常捕获的话则以Exception抛出异常。 GetAwaiter().GetResult()方法以Exception抛出异常。
测试案例1:
#region 通过Wait捕获异常
///
/// 通过wait可以捕获Task内部的异常
///
public static void WaitException()
{
try
{
//和线程不同,Task中抛出的异常可以捕获,但是也不是直接捕获,而是由调用Wait()方法或者访问Result属性的时候,由他们获得异常,将这个异常包装成AggregateException类型,或者直接以Exception,抛出捕获。
//默认情况下,Task任务是由线程池线程异步执行。要知道Task任务的是否完成,可以通过task.IsCompleted属性获得,也可以使用task.Wait来等待Task完成。
Task t = Task.Run(() => TestException());
t.Wait();
}
catch (Exception ex)
{
var a = ex.Message; //a的值为:发生一个或多个错误。
var b = ex.GetBaseException(); //b的值为:Task异常测试
Console.WriteLine(a + "|*|" + b);
}
}
static void TestException()
{
throw new Exception("Task异常测试");
}
#endregion
②但是如果没有返回结果,或者不想调用Wait()方法,该怎么获取异常呢?使用ContinueWith捕获异常(异步型捕获)(推荐)
测试案例2:
#region 通过ContinueWith设置TaskContinuationOptions参数来捕获异常(推荐)
public static void ContinueWithException(int x, int y)
{
Task<string> t = Task.Run<string>(() =>
{
Thread.Sleep(300);
Console.WriteLine("我是线程还在异步执行");
return Sumt(x, y).ToString();
});
//NotOnFaulted表示如果没有异常,才会执行ContinueWith内部的代码,但此时线程不会阻塞
//t.ContinueWith(r =>
//{
// string Exception = Convert.ToString(t.Exception);
// Console.WriteLine("异常信息1:" + Exception);
//}, TaskContinuationOptions.NotOnFaulted);
//Console.WriteLine("继续异步执行1");
//OnlyOnFaulted表示如果有异常,才会执行ContinueWith内部的代码,但此时线程不会被阻塞
t.ContinueWith(r =>
{
//Thread.Sleep(3000);
string Exception = Convert.ToString(t.Exception);
Console.WriteLine("异常信息2:" + Exception);
}, TaskContinuationOptions.OnlyOnFaulted);
Console.WriteLine("继续异步执行2");
//askContinuationOptions.OnlyOnFaulted表示:指定只应在延续任务前面的任务引发了未处理异常的情况下才安排延续任务。 此选项对多任务延续无效。【即:只有在发生异常的情况下才将异常信息记录到日志】
}
private static int Sumt(int x, int y)
{
return x / y;
}
#endregion
测试结果:
上面使用起来比较麻烦,添加一个扩展方法:
AggregateException捕获多线程中所有异常。AggregateException是一个集合。
public static Task Catch(this Task task)
{
return task.ContinueWith<Task>(delegate(Task t)
{
if (t != null && t.IsFaulted)
{
AggregateException exception = t.Exception;
Trace.TraceError("Catch exception thrown by Task: {0}", new object[]
{
exception
});
}
return t;
}).Unwrap();
}
public static Task<T> Catch<T>(this Task<T> task)
{
return task.ContinueWith<Task<T>>(delegate(Task<T> t)
{
if (t != null && t.IsFaulted)
{
AggregateException exception = t.Exception;
Trace.TraceError("Catch exception thrown by Task: {0}" , new object[]
{
exception
});
}
return t;
}).Unwrap<T>();
}
③全局捕获Task中未观察到的异常
TaskScheduler.UnobservedTaskException += (object sender, UnobservedTaskExceptionEventArgs e)=> {
Console.WriteLine("捕获异常,"+e.Exception.InnerException.Message);
};
测试案例3: 把测试案例1中的 t.Wait()方法注释掉,看能不能被全局捕捉。
测试结果:
图一:
图二:
①async…await可以捕捉异常
try{
await task1;
}
catch{
}
②C# 异步方法,尽量避免使用async void而是要用async Task
async void 方法引发的任何异常都会直接在 SynchronizationContext(在 async void 方法启动时处于活动状态)上引发,无法捕获从 async void 方法引发的异常。,从而引发程序崩溃。
下面代码无法使用 Catch 捕获来自 Async Void 方法的异常
private async void ThrowExceptionAsync()
{
throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
try
{
ThrowExceptionAsync();
}
catch (Exception)
{
// The exception is never caught here!
throw;
}
}
我们继续往下看:
//Startup.cs
public Startup()
{
Log("startup ctor");
LogBuffer.Clear();
LogBuffer.Push("other message");
}
async void Log(string message)
{
await LogBuffer.Push(message);
}
如果其它人引用你的库,他看到的 Log 函数的声明是这样的:
不太容易注意到这是个异步方法。对于一个 C# 程序员,这里很容易被认为是一个正常函数而非 async void,try catch 捕捉不到错误。
为了解决上面提出的问题,可以参考 youtube 上一个开发者建议的方法,使用一个 SafeFireAndForget 扩展函数。
//Startup.cs
public Startup()
{
Log("startup ctor").SafeFireAndForget();
}
//改成 async Task
async Task Log(string message)
{
await LogBuffer.Push(message);
}
这样做可以让使用 Log 的人看到这是一个异步方法(Task),而且在使用 Log 的地方也很容易注意到 SafeFireAndForget 进而提醒使用者这里是一个async Task 。
async void 在一种特定情况下十分有用: 异步事件处理程序。 语义方面的差异对于异步事件处理程序十分有意义 。我喜欢采用的一个方法是尽量减少异步事件处理程序中的代码(例如,让它等待包含实际逻辑的 async Task 方法),因为如果代码量过多发生异常而没有捕捉的话也会导致程序崩溃。 下面的代码演示了这一方法,该方法通过将 async void 方法用于事件处理程序而不牺牲可测试性:
private async void button1_Click(object sender, EventArgs e)
{
await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
// Do asynchronous work.
await Task.Delay(1000);
}