为什么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?"; }