为什么Task.WaitAll()不会阻塞或导致死锁?

在下面的示例中,使用了两个await调用。 为了获得性能,样本转换为Task.WaitAll() (实际上没有更快,但这只是一个例子)。

这是来自Android上使用Sqlite.Net的库中的代码,该方法从主UI线程上的OnResume()调用:

 public async Task SetupDatabaseAsync() { await CreateTableAsync(); await CreateTableAsync(); } 

这是替代方案:

 public void SetupDatabaseAsync() { var t1 = CreateTableAsync(); var t2 = CreateTableAsync(); Task.WaitAll(t1, t2); } 

但是根据我的理解, Task.WaitAll()应该在等待时阻止UI线程,从而导致死锁。 但它的工作正常。 那是因为这两个调用实际上并没有在UI线程上调用任何东西吗?

如果我使用Task.WhenAll()而不是有什么区别? 我猜它即使调用UI线程也能工作,就像await

我在博客上描述了死锁情况的细节 。 我还有一篇关于SynchronizationContext的MSDN文章 ,您可能会发现它很有帮助。

总之, Task.WaitAll将在您的方案中死锁,但Task.WaitAll是任务需要同步回UI线程才能完成。 您可以得出结论, CreateTableAsync()不会同步回UI线程。

相反,这段代码会死锁:

 public async Task SetupDatabaseAsync() { await CreateTableAsync(); await CreateTableAsync(); } Task.WaitAll(SetupDatabaseAsync()); 

我建议你不要阻止异步代码; 在async世界中,同步回到上下文是默认行为(正如我在async介绍中描述的那样),因此很容易意外地执行它。 未来对Sqlite.Net的一些更改可能(意外地)同步回原始上下文,然后任何使用Task.WaitAll代码Task.WaitAll像原始示例一样突然死锁。

最好“一直”使用async

 public Task SetupDatabaseAsync() { var t1 = CreateTableAsync(); var t2 = CreateTableAsync(); return Task.WhenAll(t1, t2); } 

“异步一直”是我在异步最佳实践文章中推荐的指南之一。

当你阻塞UI线程(和当前的同步上下文)时,如果你正在等待的任务之一将委托给当前上下文然后等待它(同步或异步),它将只会导致死锁。 在任何异步方法上同步阻塞并不是每种情况下的即时死锁。

因为默认情况下, async方法会将方法的其余部分封送到当前同步上下文并且在每次单独await ,并且因为任务在此之前永远不会完成,这意味着同步等待使用async/await方法通常会僵局; 至少除非明确覆盖所描述的行为(通过,例如ConfigureAwait(false) )。

使用WhenAll意味着您没有阻止当前的同步上下文。 所有其他任务都完成后,不再阻止线程,而是仅调度另一个延续,让上下文自由处理现在准备好的任何其他请求(例如,从底层async方法继续执行WhenAll等待的时候)。

也许这个样本将展示可能发生的事情。 这是一个iOS视图加载。 尝试使用await调用和不使用它(下面已注释掉)。 在该函数中没有任何等待它将同步运行并且UI将被阻止。

  public async override void ViewDidLoad() { base.ViewDidLoad (); var d1 = Task.Delay (10); var d2 = Task.Delay (10000); //await Task.Delay (10); Task.WaitAll (d1, d2); this.label.Text = "Tasks have ended - really!"; } public override void ViewWillAppear(bool animated) { base.ViewWillAppear (animated); this.label.Text = "Tasks have ended - or have they?"; }