【问题标题】:Task.WaitAll() deadlockingTask.WaitAll() 死锁
【发布时间】:2019-02-19 14:50:55
【问题描述】:

我想在 xUnit 测试中多次调用异步方法并等待所有调用完成,然后再继续执行。我读到我可以在这种情况下使用Task.WhenAll()Task.WaitAll()。然而,由于某种原因,代码出现了死锁。

[Fact]
public async Task GetLdapEntries_ReturnsLdapEntries()
{
    var ldapEntries = _fixture.CreateMany<LdapEntryDto>(2).ToList();
    var creationTasks = new List<Task>();
    foreach (var led in ldapEntries)
    {
        var task = _attributesServiceClient.CreateLdapEntry(led);
        task.Start();
        creationTasks.Add(task);
    }
    Task.WaitAll(creationTasks.ToArray()); //<-- deadlock(?) here
    //await Task.WhenAll(creationTasks);

    var result = await _ldapAccess.GetLdapEntries();

    result.Should().BeEquivalentTo(ldapEntries);
}

public async Task<LdapEntryDto> CreateLdapEntry(LdapEntryDto ldapEntryDto)
{
    using (var creationResponse = await _httpClient.PostAsJsonAsync<LdapEntryDto>("", ldapEntryDto))
    {
        if (creationResponse.StatusCode == HttpStatusCode.Created)
        {
            return await creationResponse.Content.ReadAsAsync<LdapEntryDto>();
        }

        throw await buildException(creationResponse);
    }
}

被测系统是一个封装了 HttpClient 的包装器,它调用 Web 服务,awaits 响应,并且可能 awaits 读取响应的内容,最终反序列化并返回。

当我将测试中的foreach 部分更改为以下(即不要使用Task.WhenAll() / WaitAll())时,代码运行时没有死锁:

foreach (var led in ldapEntries)
{
    await _attributesServiceClient.CreateLdapEntry(led);
}

到底发生了什么?

编辑:虽然这个问题已被标记为重复,但我看不出链接的问题与这个问题有何关系。链接中的代码示例都使用.Result,据我所知,它会阻止执行,直到任务完成。相反,Task.WhenAll() 返回一个可以等待并在所有任务完成后完成的任务。那么为什么等待Task.WhenAll()死锁呢?

【问题讨论】:

  • @Servy 感谢您的链接,但我看不到与Task.WhenAll() / WaitAll() 的关系?
  • 副本准确地解释了为什么会发生死锁,以及如何处理它。只需阅读答案即可。
  • 这就是我在评论之前所做的,我仍然看不到与WaitAllWhenAll 的关系,后者创建了一个等待的任务并且仍然死锁;我不在任何地方使用.Result。遗憾地告诉我“再读一遍”对我没有帮助。
  • 您在使用 Task.WaitAll 时是否收到编译器警告?我怀疑你是,因为你的方法被标记为异步。它不是在等待。它也没有返回一个任务,所以它看起来甚至不会编译。无论如何,绝对不应该使用 Task.WaitAll。这是什么类型的应用程序(ASP.NET、ASP.NET Core、WPF、Win Forms 等...)?
  • @mason 哪一个?所有方法都返回TaskTask&lt;T&gt;Task.WaitAll() 等不及了。我没有收到任何编译器警告或错误,此代码工作正常(死锁除外,使用备用 foreach 版本按预期工作)。

标签: c# async-await task


【解决方案1】:

您发布的代码不可能有描述的行为。第一次调用Task.Start 会抛出InvalidOperationException,导致测试失败。

我读到我可以在这种情况下使用 Task.WhenAll() 和 Task.WaitAll()。

没有;要异步等待多个任务,您必须使用Task.WhenAll,而不是Task.WaitAll

例子:

[Fact]
public async Task GetLdapEntries_ReturnsLdapEntries()
{
  var ldapEntries = new List<int> { 0, 1 };
  var creationTasks = new List<Task>();
  foreach (var led in ldapEntries)
  {
    var task = CreateLdapEntry(led);
    creationTasks.Add(task);
  }
  await Task.WhenAll(creationTasks);
}

public async Task<string> CreateLdapEntry(int ldapEntryDto)
{
  await Task.Delay(500);
  return "";
}

【讨论】:

  • 感谢您的输入,但它没有抛出任何异常,我已经多次运行测试,总是遇到死锁。明天我会用你的例子试试。
  • 正如我试图在原始问题中解释的那样,Task.WhenAll 如您所建议的那样使用是死锁
  • @Thaoden:我已经更新了代码。我已经在没有死锁的情况下运行了这个确切的代码。
  • 我同时做了,发现底层错误在_httpClient调用的网络服务中。感谢您的帮助!
【解决方案2】:

Task.WaitAll() 会死锁,因为它在任务未完成时阻塞了当前线程(并且由于您使用的是async/await 而不是线程,因此您的所有任务都在同一线程,并且您不会让等待的任务返回调用点,因为它们正在运行的线程 -与您调用 Task.WaitAll()-的线程相同)。

不知道为什么WhenAll 也会在这里为您陷入僵局,但绝对不应该。

PS:您不需要在 async 方法返回的任务上调用 Start:它们在创建时就已经“热”(已经开始)了

【讨论】:

    猜你喜欢
    • 2018-08-26
    • 1970-01-01
    • 1970-01-01
    • 2017-12-27
    • 2014-06-08
    • 2013-11-30
    • 2013-12-25
    • 2015-01-29
    • 2017-03-02
    相关资源
    最近更新 更多