Task的问题
在C#中异步Task是一个很方便的语法,经常用在处理异步,例如需要下载等待等方法中,不用函数跳转,代码阅读性大大提高,深受大家喜欢。
但是有时候发现我们的异步函数可能出现了报错,但是异常又没有抛出,导致我们不知道,我们下面来根据代码来分析解决。
正常使用中的问题
我们通常这样使用:
async void BeginTest()
{
Loger.Debug($"测试开始,{Environment.CurrentManagedThreadId}");
await asynctest();
Loger.Debug($"测试结束,{Environment.CurrentManagedThreadId}");
}
async Task asynctest()
{
Loger.Debug($"异步开始,{Environment.CurrentManagedThreadId}");
await Task.Delay(3000);
Loger.Debug($"异步等待,{Environment.CurrentManagedThreadId}");
int a = 0;
int b = 1 / a; //这里会报错
Loger.Debug("异步结束");
}
我们使用await没有任何问题,如果出错了会中止,并抛出异常。
但是如果我们把调用代码的await去掉,改成如下:
async void BeginTest()
{
Loger.Debug($"测试开始,{Environment.CurrentManagedThreadId}");
_= asynctest();
Loger.Debug($"测试结束,{Environment.CurrentManagedThreadId}");
}
我们会发现没有任何报错,这样我们的逻辑出现为题就不会被发现。
解决办法1 - void
对于这种不需要返回值的Task我们可以定义成void类型。
async void BeginTest()
{
Loger.Debug($"测试开始,{Environment.CurrentManagedThreadId}");
asynctest();
Loger.Debug($"测试结束,{Environment.CurrentManagedThreadId}");
}
async void asynctest()
{
Loger.Debug($"异步开始,{Environment.CurrentManagedThreadId}");
await Task.Delay(3000);
Loger.Debug($"异步等待,{Environment.CurrentManagedThreadId}");
int a = 0;
int b = 1 / a; //这里会报错
Loger.Debug("异步结束");
}
运行后也可以抛出错误。
解决办法2 - UnobservedTaskException
如果我们就是要使用 _= asynctest()的方式调用,不想中断程序。那么这样相当于没有对任务持续观察,所以捕获不到问题,我们可以添加这样的方法来捕获。
TaskScheduler.UnobservedTaskException += TaskSchedulerUnobservedTaskException;
private static void TaskSchedulerUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
{
e.SetObserved();
Exception ex = e.Exception;
Loger.Debug(ex.Message+ ",StackTrace:" + ex.StackTrace);
}
我们的调用函数可以这样,需要额外加一个gc。
async void BeginTest()
{
Loger.Debug($"测试开始,{Environment.CurrentManagedThreadId}");
_= asynctest();
Loger.Debug($"测试结束,{Environment.CurrentManagedThreadId}");
await Task.Delay(5000);
Loger.Warn("gc");
System.GC.Collect();
//等待终结器处理
GC.WaitForPendingFinalizers();
}
当GC的时候,并且task没引用,这时候才会进入UnobservedTaskException 。 我们能看到报错的原因,但是发现没有堆栈追踪数据,StackTrace是null的。
解决办法3 - Forget
我们自定义一个观察任务的方法
public static class TaskExtensions
{
/// <summary>
/// Observes the task to avoid the UnobservedTaskException event to be raised.
/// </summary>
public static void Forget(this Task task)
{
// note: this code is inspired by a tweet from Ben Adams: https://twitter.com/ben_a_adams/status/1045060828700037125
// Only care about tasks that may fault (not completed) or are faulted,
// so fast-path for SuccessfullyCompleted and Canceled tasks.
if (!task.IsCompleted || task.IsFaulted)
{
// use "_" (Discard operation) to remove the warning IDE0058: Because this call is not awaited, execution of the current method continues before the call is completed
// https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/discards?WT.mc_id=DT-MVP-5003978#a-standalone-discard
_ = ForgetAwaited(task);
}
// Allocate the async/await state machine only when needed for performance reasons.
// More info about the state machine: https://blogs.msdn.microsoft.com/seteplia/2017/11/30/dissecting-the-async-methods-in-c/?WT.mc_id=DT-MVP-5003978
async static Task ForgetAwaited(Task task)
{
try
{
// No need to resume on the original SynchronizationContext, so use ConfigureAwait(false)
await task.ConfigureAwait(false);
}
catch (Exception ex)
{
// Nothing to do here
Loger.Warn("??????????"+ex.Message+", stacktrace: "+ex.StackTrace);
}
}
}
}
调用函数这样:
async void BeginTest()
{
Loger.Debug($"测试开始,{Environment.CurrentManagedThreadId}");
asynctest().Forget();
Loger.Debug($"测试结束,{Environment.CurrentManagedThreadId}");
}
运行后结果:
这样程序不会被打断还有报错输出。
参考
https://discussions.unity.com/t/async-and-uncaught-exceptions/824272/14
https://www.youtube.com/watch?v=ZFWxSQ-KjUc