为什么使用ConfigureAwait(false)不起作用,而Task.Run()却能起作用?

13

我正在使用 .ConfigureAwait(false) 调用异步库方法,但仍然遇到了死锁问题。 (我在ASP.NET控制器API中使用它)

但是,如果我将相同的方法包装在 Task.Run() 中,它就可以正常工作。

我的理解是,如果库方法内部没有使用 ConfigureAwait,那么添加 ConfigureAwait 不会解决问题,因为在库调用中它会导致死锁(我们通过使用 .Result 阻塞它)。但是,如果是这种情况,为什么 Task.Run() 可以正常工作呢?虽然它将无法在相同的上下文/线程中继续执行。

这篇文章有讲述到。顺便说一句,我已经阅读过不少Stephen Cleary的文章,但为什么Task.Run()能够正常工作还是个谜。

代码片段:

// This Create Method results in Deadlock
public async Task<string> Create(MyConfig config)
{
        Document doc = await Client.CreateDocumentAsync(CollectionUri, config).ConfigureAwait(false);
        return doc.Id;
}

// Uses Task.Run() which works properly, why??
public string Create(MyConfig config)
{
    Document doc = Task.Run(() => Client.CreateDocumentAsync(CollectionUri, config)).Result;
    return doc.Id;
}

[HttpPost]
public ActionResult CreateConfig(MyConfig config)
{
     string id = Create(config).Result;
     return Json(id);
}
3个回答

16

我认为Lukazoid是正确的。换句话说...

// This Create Method results in Deadlock
public async Task<string> Create(MyConfig config)
{
  Document doc = await Client.CreateDocumentAsync(CollectionUri, config).ConfigureAwait(false);
  return doc.Id;
}

仅仅在一个地方添加 ConfigureAwait(false) 并不能神奇般地防止死锁。只有当每个 await 在该方法的可传递闭包及其调用的所有方法中使用 ConfigureAwait(false) 时,ConfigureAwait(false) 才能防止死锁。

换句话说,在 Create 中的每个 await 都需要使用 ConfigureAwait(false)(这是已经有的),并且在 CreateDocumentAsync 中的每个 await 也都需要使用它(我们不知道),还需要在每个调用 CreateDocumentAsync 的方法中的每个 await 中使用 ConfigureAwait(false) ,以此类推。

这就是为什么它是如此脆弱的解决方案,只是一个“解决”死锁问题的原因。


1
每个异步调用都应该在这方面进行充分的文档记录,以便做出正确的决策。可以使用await [async call].ConfigureAwait(false)或者await Task.Run(async ()=> await [async call]).ConfigureAwait(false) - Thanasis Ioannidis
1
@ThanasisIoannidis: 理论上来说那样做很好,但在现实中缺少 ConfigureAwait(false) 通常是一个错误。因此文档可能会出错。 - Stephen Cleary
1
我们可以说“all the way configreawait(false)”就像说“all the way await”一样吗? - Emil
2
@batmaci:是的,如果你要使用它,最好全程使用。 - Stephen Cleary

8
在第一个例子中,Client.CreateDocumentAsync 的实现出现了死锁,因为它试图使用当前的SynchronizationContext执行延续。当使用Task.Run时,委托将在ThreadPool线程上调用,这意味着没有当前的SynchronizationContext,所以所有延续都将使用ThreadPool线程恢复。这意味着它不会死锁。
顺带问一下,为什么您的CreateConfig方法不是异步的?最近的MVC和WebAPI版本都支持异步方法,摆脱.Result将是最好的解决方案。

1
使用ConfigureAwait(false)不应该确保使用线程池进行继续吗?是的,我已经将CreateConfig方法设置为异步。但是,我想更好地理解这个概念,确切地了解为什么Task.Run()不会创建任何问题。 - Krunal Modi
5
ConfigureAwait(false)仅配置续订使用线程池,而不是Client.CreateDocumentAsync的调用。 - Lukazoid
@KrunalModi 如果一个异步调用内部有另一个异步调用,而该调用未使用ConfigureAwait,那么使用ConfigureAwait是没有意义的。ConfigureAwait(false) 几乎可以在任何地方使用,特别是在库中。只有顶层调用(在UI同步上下文中的调用)不需要 ConfigureAwait(false),因为继续执行应在UI同步上下文中执行。对于不在内部使用 ConfigureAwait(false) 的库,使用 await Task.Run(()=> ...)await Task.Run(()=> ...).ConfigureAwait(false) 是一个很好的解决方法。 - Thanasis Ioannidis

0

仅仅是一点观察:我也注意到这样做会导致死锁。

private string Create(Task<Document> task)
{
    var doc = Task.Run(() => task).Result;
    return doc.Id;
}

[HttpPost]
public ActionResult CreateConfig(MyConfig config)
{
     var task = Client.CreateDocumentAsync(CollectionUri, config);
     var id = Create(task).Result;
     return Json(id);
}

即使在线程池上运行代码也可能不是终极解决方案。似乎同样重要的因素是考虑在创建异步方法任务时的SynchonizationContext


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接