【问题标题】:Async/Await and Web API blocks executionAsync/Await 和 Web API 阻止执行
【发布时间】:2019-08-07 21:47:53
【问题描述】:

我的 REST API 服务器中有以下内容(使用 OWIN):

    [ResponseType(typeof(Session))]
    [Route("api/v1/async")]
    public Session async_status()
    {
        AsyncStatus();
        return new Session() {...};
    }

    private async Task<int> AsyncStatus()
    {
        await SyncMethod();
        return 42;
    }

    private Task<int> SyncMethod()
    {
        CopyFileThatTakesALongTime(....);
        // OR
        Thread.Sleep(60000);
        return Task.FromResult(42);
    }

为什么死掉它会阻塞并且客户端没有获得所需的异步性。我希望在调用时:

await SyncMethod();

调用者将被释放并解除阻塞,而其余的睡眠将被执行。我错过了什么?

P.S:我可以通过替换它来轻松修复它:

AsyncStatus();

与:

Task.Run(() => AsyncStatus() );

我不这样做的原因是因为阅读:

https://blog.stephencleary.com/2013/11/taskrun-etiquette-examples-using.html

换句话说,如果像上面文章中那样在 Web 服务器中使用 Task.Run() 很糟糕,但是您不能使用 async/await,那么解决方案是什么?

【问题讨论】:

  • 您是否试图模拟从异步代码调用某些阻塞 API?首先,异步应该传播到调用堆栈的顶部(因此您应该将控制器的公共方法标记为异步并在异步调用上执行等待)。其次,应不惜一切代价避免使用 Thread.Sleep。在异步世界中,它被“await Task.Delay(...)”取代
  • 似乎您正在尝试“解雇并忘记”。阅读this。
  • 一切都是票价 Api 控制器是公开的
  • 不确定你想用这段代码完成什么。看起来您正在将任务(协程)的概念与线程混合在一起。这是软件中多任务处理的两种截然不同的方法。使用 await Task.Delay(...) 会将 SyncMethod 修复为异步兼容(但我不会将其称为同步方法)。最后, async_status() 方法会触发并忘记,因此无论如何都不会在该方法中挂起。您能否为您要完成的工作提供更多背景信息?
  • 我想要完成的非常简单。让我们举一个真实的例子。假设您想开发一个 API 来指示服务器将大文件从位置 A 复制到 B。您不希望调用被阻塞,而只是触发副本,仅此而已。另一个要求之一是事后能够知道副本的进度,但我们暂时不处理这部分。

标签: c# asp.net async-await


【解决方案1】:

async 方法仅在异步工作开始时变为异步。

也就是说,您的所有代码都将同步运行,直到真正异步开始。一旦一些真正的异步工作开始,一个Task 会在调用堆栈中返回,然后调用方法可以等待(或不等待)。

这意味着如果你在异步工作之前做了很多同步工作,异步方法仍然可以阻止请求。你的例子完美地说明了这一点。让我们分解一下线程在做什么:

  1. 开始运行async_status()
  2. 跳转到AsyncStatus()
  3. 在await SyncMethod(),线程跳转到SyncMethod()。
  4. 线程在 Thread.Sleep(60000) 处停止 60 秒。
  5. SyncMethod() 返回一个完整的 Task。
  6. 因为没有等待,AsyncStatus() 同步继续,并返回一个值。
  7. async_status() 返回请求。

Thread.Sleep(60000) 可以替换为任何处理器密集型任务(或任何长时间运行的非异步任务),如果它发生在该方法中的第一个 await 之前,结果将是相同的。

这在 Microsoft 的文档中也有描述,标题为 What happens in an async method。注意点3:

  1. GetStringAsync 中发生的事情暂停了其进度。也许它必须等待网站下载或其他一些阻止活动。为避免阻塞资源,GetStringAsync 将控制权交给其调用者 AccessTheWebAsync。

控制仅在返回Task 时返回调用方法,这仅在实际启动异步任务时发生。在您的示例中,没有发生任何异步任务。如果您添加了一个(即使只是await Task.Delay(1)),您会看到不同的结果。例如,这将立即返回:

[ResponseType(typeof(Session))]
[Route("api/v1/async")]
public Session async_status()
{
    AsyncStatus();
    return new Session() {...};
}

private async Task<int> AsyncStatus()
{
    await SyncMethod();
    return 42;
}

private async Task<int> SyncMethod()
{
    await Task.Delay(1);
    Thread.Sleep(60000);
    return 42;
}

在await Task.Delay(1)(或任何真正异步工作):

  1. SyncMethod() 将不完整的 Task 返回到 AsyncStatus()
  2. AsyncStatus() 将不完整的 Task 返回到 async_status()。
  3. Task 没有等待,所以async_status() 的执行继续

回答您的问题:

你问:

如果在Web服务器中使用Task.Run()很糟糕,但你不能使用async/await,解决办法是什么?

您提到了Task.Run Etiquette 文章。最后阅读他的结论:

不要仅仅为了“为我的异步方法提供可等待使用的东西”而使用它。

您在这里的目的不仅仅是“提供可等待的东西”,而是启动后台作业,而不是让客户端等到它完成。那是不一样的。

所以是的,您实际上可以使用Task.Run()。但是,正如 Stephen Cleary 的 Fire and Forget on ASP.NET 文章中所述,这样做的问题是 ASP.NET 无法跟踪该后台任务。因此,如果它长时间运行,那么回收应用程序池可能会立即终止这项工作。他的文章描述了执行后台工作的更好方法,您不想阻止请求的返回。

【讨论】:

  • 即使我用一个很长的for循环替换了sleep,它只会在循环完成后返回给调用者。
  • 实际上,这也确实解释了您问题中的示例......我应该更新我的答案以更清楚。
  • 你更新了你的答案,但这不是我的意思,我的意思是你会将你添加的 foreach 放在 SyncMethod() 中(而不是 AsyncStatus 中),然后我希望等待 SyncMethod () 在 AsyncStatus 中将控件返回给调用者,因为正如您所说的那样等待。请注意,我对其进行了测试,它的行为与我描述的一样,因此在等待之后控件不会返回给调用者。
  • @isaac.hazan:这是正确答案。总结一下:await first examines its awaitable, and if it is already complete, then the async method just continues synchronously。您可以将 Thread.Sleep 更改为 await Task.Delay 以查看如果您将其更改为实际异步代码会如何运行。
  • 所以在您的代码中,唯一一次返回 Task 是在所有等待完成之后。即使您将Thread.Sleep 替换为循环或复制文件,它也会忙于执行此操作而不返回Task,因此await 无事可做。您可以使用我的答案中的代码块来测试我在说什么。您可以将Task.Delay(1) 放在Thread.Sleep 之前的任何位置(或大循环,或复制文件),它会按预期工作 - 因为Task.Delay 将返回Task 之前 工作完成了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-07-24
  • 1970-01-01
  • 2021-11-18
  • 1970-01-01
  • 2015-09-28
  • 2016-02-08
  • 1970-01-01
相关资源
最近更新 更多