当您在不使用await关键字调用异步方法时,会发生什么?

19

我有一个Web服务器,定期执行作业以合并和发送记录(大量请求日志)。

Task.Run(() =>
{
    while (true)
    {
        try
        {
            MergeAndPutRecords();
        }
        catch (Exception ex)
        {
            Logger.Error(ex);
        }
    }
});
MergeAndPutRecords函数中,有合并记录的代码和异步函数返回发送记录的任务。(实际上这是Amazon Kinesis Firehose的PutRecordBatchAsync。)
如果我在不使用await关键字的情况下调用该函数会发生什么?函数是否在单独的线程上运行? 这里说它不会。那么返回的任务意味着什么? 这里表示不带await关键字的异步方法意味着:
  1. 在当前线程上启动异步方法。忽略所有结果(包括异常)。
那么我的定期作业和PutRecordBatchAsync会同时处理吗?我知道异步和并发是不同的。但是没有await关键字,它们在同一个线程中。哪个会先执行?我很困惑...
需要合并并实时发送大量记录。所以我认为必须同时执行...

4
async 关键字本身不起作用。每个 await 被组合成一个 AsyncStateMachine,可以异步处理内容。如果在 async 方法中没有 await 关键字,则没有创建 AsyncStateMachine 的必要,因此您将得到同步方法。 - FCin
4
await 被用来等待结果的返回。它不会以任何方式控制获取结果的过程是如何启动或正在进行的获取结果的过程是如何工作的。 - Damien_The_Unbeliever
2个回答

8
那么我的定期工作和PutRecordBatchAsync会同时处理吗?
使用Task API,您可以确保它们并发执行(使用线程池),但是您需要了解内存并发操作与基于IO的并发操作之间的区别。
虽然在内存中的并发操作可以受益于使用Tasks,但一旦执行IO调用,就根本不需要线程,因为它依赖于硬件并发性,如果它使用线程,那么它所做的就是等待IO调用返回,从而浪费宝贵的系统资源并降低系统可伸缩性。
你的情况是基于IO的并发,因为你调用远程/网络API,异步等待如何帮助你?
真正的异步操作将释放线程上下文,在Windows上,它将使用IO完成端口(排队机制)来执行异步调用,而调用线程用于分派其他类似的调用,只需要在线程上下文返回时用于服务响应,对于这个问题,如果它不是UI调用,则使用ConfigureAwait(false),以便使用任何线程上下文来传递响应。
如果您不使用await和async会怎样?
意味着异步的调用变成同步的,会立即影响系统的可伸缩性,因为线程现在被阻塞了,对于长时间运行的IO操作来说更糟糕。您是否见过JavaScript框架如何始终向服务器API发出AJAX(异步)调用,因此可以在不阻塞浏览器线程的情况下完成更多工作。
一般来说,在内存处理中,您将创建某个数量的Tasks并使用Task.WaitAll或Parallel.ForEach处理它们,对于异步处理,理想情况下,建议不要在任何地方使用Task.Run,最好从入口点开始使用Async,就像在MVC的情况下那样,控制器可以是async的。多个调用使用Task.WhenAll表示任务进行分组,然后等待其完成。即使在代码中使用Task.Run,也使用async lambda执行异步调用。
总结:
必须使用await进行异步调用,否则在该上下文中async关键字无效,而且是的,await将等待IO调用返回,然后才会执行继续执行,但没有线程被阻塞。

2
“原本应该是异步的调用变成了同步,这会立即影响系统的可扩展性,因为线程现在被阻塞了。” 我认为这可能不正确或不清楚。是的,对异步函数的调用返回同步,但从概念上讲,它总是这样做的;异步性发生在await语句处。如果没有await,调用者将按顺序继续执行异步函数之外的操作。如果任务有后续操作,它仍然会运行,但实际上是无头的;结果和异常都会被忽略。 - shannon
1
因此,大多数异步处理的真正案例仍然是IO处理,我们可以使用ConfigureAwait(false)来确保返回调用可以在任何可用的线程上执行,而不是等待同一线程上下文进行重新进入,这是一项性能优化。另外,异常和结果没有被忽略,但可以通过显式访问Task获得,这是在.Net 4.5中设计的,与早期版本不同,当异常自动传播时就像其他逻辑代码一样。 - Mrinal Kamboj
2
我不明白你所说的“好处”会失去的意思。如果我们知道该方法不需要等待,那么不等待它并不会降低可扩展性。请注意,这个问题是关于异步方法而不是异步调用的。如果一个异步方法负责倒垃圾,并保证在不到一小时内完成,返回值是垃圾被倒出时的气味,那么每周“启动并忘记”任务是完全可以接受的,如果我们认为它符合我们的要求。 - shannon
1
@MrinalKamboj,OP并没有暗示使用Task.Wait()Task.Result来代替使用await。相反,他们正在询问如果从代码中省略await会发生什么。如果省略了await,则不会出现阻塞或扩展问题,并且任务在将来的某个时间点执行,当TaskScheduler处理它时。它有效地成为一个“fire and forget”任务,其异常转到TaskScheduler.UnobservedExceptions并被忽略。 - Dave Black
2
@MrinalKamboj 除非您使用 Task.Run(),否则不会使用线程池。省略 await 将在当前线程上启动异步方法。它将忽略所有结果(包括异常)。- 请参见此处:https://dev59.com/31YO5IYBdhLWcg3wTPrk#46245804 - Dave Black
显示剩余7条评论

0
如果一个方法返回一个Task,最好的做法是在某个时刻观察该Task的结果。这可能是声明的返回值,也可能是异常。如果你想在方法继续之前观察该任务,建议使用await
在这种情况下,您应该观察由PutRecordBatchAsync返回的Task的结果。您需要知道是否因为任何原因调用失败,因为这可能表示您的记录没有被存储!
在您提供的示例代码中,您会发现在上一次调用完成之前,您进行了后续调用MergeAndPutRecords。您确定这是有意的吗?

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