主要区别在于异常传播。 一个例外,内抛出async Task
方法,获取存储在返回的Task
对象和直到任务被通过观察保持休眠await task
,task.Wait()
,task.Result
或task.GetAwaiter().GetResult()
。即使从同步部分抛出,也会以这种方式传播async
。
考虑下面的代码,其中OneTestAsync
和AnotherTestAsync
表现完全不同:
static async Task OneTestAsync(int n)
{
await Task.Delay(n);
}
static Task AnotherTestAsync(int n)
{
return Task.Delay(n);
}
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
Task task = null;
try
{
task = whatTest(n);
Console.Write("Press enter to continue");
Console.ReadLine();
task.Wait();
}
catch (Exception ex)
{
Console.Write("Error: " + ex.Message);
}
}
如果我打电话 DoTestAsync(OneTestAsync, -2)
,它将产生以下输出:
按Enter继续
错误:发生一个或多个错误。等待Task.Delay
错误:第二
注意,我必须按一下Enter才能看到它。
现在,如果我调用DoTestAsync(AnotherTestAsync, -2)
,内部的代码工作流程DoTestAsync
就大不相同了,输出也是如此。这次,我没有被要求按Enter:
错误:该值必须为-1(表示无限超时),0或正整数。
参数名称:millisecondsDelayError:1st
在这两种情况下Task.Delay(-2)
,在验证其参数时都在开始时抛出。这可能是一个虚构的场景,但理论上Task.Delay(1000)
也可能会抛出异常,例如,当基础系统计时器API发生故障时。
在一个侧面说明,误差传播逻辑为尚未不同async void
的方法(而不是async Task
方法)。如果当前线程有一个(。,它将async void
通过SynchronizationContext.Post
)重新抛出异常,方法内部引发的异常将立即通过()通过当前线程的同步上下文SynchronizationContext.Current != null)
重新抛出ThreadPool.QueueUserWorkItem
。调用者没有机会在同一堆栈帧上处理此异常。
我在这里和这里发布了有关TPL异常处理行为的更多详细信息。
问:是否可以模仿基于async
非异步Task
方法的方法的异常传播行为,以使后者不会抛出相同的堆栈帧?
答:如果确实需要,那么可以,有一个窍门:
async Task<int> MethodAsync(int arg)
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
}
Task<int> MethodAsync(int arg)
{
var task = new Task<int>(() =>
{
if (arg < 0)
throw new ArgumentException("arg");
return 42 + arg;
});
task.RunSynchronously(TaskScheduler.Default);
return task;
}
但是请注意,在某些情况下(例如当堆栈太深时),RunSynchronously
仍可以异步执行。
另一个显着区别是,
在async
/await
版本更容易出现死锁定在一个非默认的同步上下文。例如,以下内容将在WinForms或WPF应用程序中死锁:
static async Task TestAsync()
{
await Task.Delay(1000);
}
void Form_Load(object sender, EventArgs e)
{
TestAsync().Wait();
}
将其更改为非异步版本,不会死锁:
Task TestAsync()
{
return Task.Delay(1000);
}
斯蒂芬·克莱里(Stephen Cleary)在其博客中很好地解释了这种僵局。
await
/async
的意义:)