Task.WaitAll 保持循环

4

我只是为了测试async关键字而尝试这段异步代码:

public async Task<string> AsyncMethod()
{
    var link = "http://www.google.com";

    var webclient = new WebClient();
    var result = await webclient.DownloadStringTaskAsync(new Uri(link));

    return result;
}

public async Task<ActionResult> Index()
{
    var a = AsyncMethod();
    var b = AsyncMethod();

    Task.WaitAll(a, b);

    return View();
}

但是当我调试它时,调试器会执行Task.WaitAll,但什么也不做(返回关键字从未执行)。。如果我在两个'AsyncMethod'之前设置await并删除Task.WaitAll,它就可以工作。那么我做错了什么?


1
WaitAll会启动任务吗?如果不是的话,那么你将会等很长时间,因为没有人启动这些任务... - flq
@flq 你说的启动任务是什么意思? - MuriloKunze
3
带有 async 修饰符的方法返回的任务已经在运行。 - e_ne
你在 return result 处设置了断点吗?也许任务在到达 Task.WaitAll 之前就已经完成了。尝试在 WaitAll 之后打印结果,看看是否有输出。另外,Index() 方法中不需要使用 async 关键字。 - MBen
我认为在Task.WaitAll之前它没有完成,下载不可能如此快,如果我删除Index的async,我会得到一个异常。 - MuriloKunze
显示剩余2条评论
3个回答

14
因为你的方法看起来像是ASP.NET MVC控制器操作,我假设你正在运行ASP.NET。
默认情况下,异步方法会在与其暂停的上下文(即您调用await的位置)恢复。在ASP.NET中,这意味着当前请求上下文。而且每次只能有一个线程在特定上下文中。所以,执行Index()的线程在请求上下文中,在WaitAll()中被阻塞。另一方面,AsyncMethod()的两个调用都试图在相同的上下文中恢复(在它们完成下载后),但是它们无法这样做,因为Index()仍然在该上下文中执行。由于这个原因,方法陷入了死锁,因此什么也没有发生。
(在GUI应用程序中也会发生相同的死锁,因为GUI上下文在这方面的行为类似。控制台应用程序没有这个问题,因为它们没有任何上下文。)
解决方法有两个方面:
  1. 永远不要同步等待异步方法。(可能唯一的例外是如果您想从控制台应用程序的 Main() 方法执行异步方法。)
  2. 相反,异步等待它们。在您的情况下,这意味着使用 await Task.WhenAll(a, b)

  3. 在您的“库”方法中使用 ConfigureAwait(false)(即那些实际上不需要在请求上下文中执行的方法)。

使用 1 或 2 将修复您的问题,但最好两者都做。

有关此问题的更多信息,请阅读 Stephen Cleary 的文章 Don't Block on Async Code


非常有用,我注意到这个问题在控制台应用程序上没有发生,但我不知道为什么。+1 - e_ne

-1

确保您的async方法使用CancellationToken

// the cancellation token from the request triggers 
// when the user cancels the HTTP request in the web browser
public async Task<IActionResult> Index(CancellationToken cancellationToken = default)
{
    // internal cancellation token for the timeout
    using var ctsTimeout = new CancellationTokenSource(TimeSpan.FromMilliSeconds(2000));

    // cancels when either the user cancels the request
    // or the timeout expires
    using var cts = CancellationToken.CreateLinkedTokenSource(cancellationToken, ctsTimeout.Token);

    // make sure your methods make use of the cancellation token internally
    // ie. check the token in loops and on I/O requests
    var a = AsyncMethod(cts.Token);
    var b = AsyncMethod(cts.Token);

    // optional: pass the token to the Task.WaitAll method
    // ensures the HTTP request completes
    // even when the internal tasks won't
    Task.WaitAll(new [] { a, b }, cts.Token);

    return View();
}

如果其中一个被调用的方法未返回,那么超时机制将确保任务被取消,Task.WaitAll 抛出一个 Exception

“超时将确保任务被取消” - 任务并不会被取消,它们只是被忽略并成为一种“fire-and-forget”的任务。CancellationToken 取消的是等待任务的操作,而不是任务本身。 - Theodor Zoulias
这就是 AsyncMethod(cts.Token) 的作用。 - MovGP0
1
我在谈论Task.WaitAll方法中传递的令牌。我认为除非你真的想要快速启动等待的任务,否则不需要它。 - Theodor Zoulias

-2

它的工作方式如下:

public Task<string> FakeAsyncMethod()
{
    var link = "http://google.com";
    var webclient = new WebClient();
    var t = new Task<string>(() => webclient.DownloadString(new Uri(link)));
    return t;
}

public async Task Index()
{
    var a = FakeAsyncMethod();
    var b = FakeAsyncMethod();
    a.Start();
    b.Start();
    Task.WaitAll(a, b);
}

async void AsyncCall()
{
    await Index();
}

我不知道为什么它不能与你的方法一起工作,但我怀疑这是因为使用 async 关键字标记的方法返回的任务处于运行状态(更准确地说,Status 等于 WaitingForActivation)。我会进一步研究。

编辑:另一种选择是使用 Task.WhenAllawait 关键字配对使用。

public async Task Index()
{
    var a = AsyncMethod();
    var b = AsyncMethod();
    await Task.WhenAll(a, b);
}

我不太明白为什么需要启动异步任务,因为当我在AsyncMethod中设置本地URL时,我注意到页面已经被下载了。 - MuriloKunze
太好了,WhenAll正是我所需要的。谢谢。 - MuriloKunze
1
在我发布的第一个示例中,您没有使用自己的方法,而是使用了我的(FakeAsyncMethod)。那个方法返回一个任务,其Status等于Created但尚未运行 - 这就是为什么您需要启动它的原因。在我的编辑中,您使用了自己的方法。 - e_ne
这并没有解释实际问题是什么,或者为什么修复措施有效。我也不明白为什么将Task保持在运行状态会成为一个问题。 - svick

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