如何在.NET中调用异步函数而不等待它[重复]

Posted

技术标签:

【中文标题】如何在.NET中调用异步函数而不等待它[重复]【英文标题】:How to call an async function in .NET without waiting for it [duplicate] 【发布时间】:2019-08-05 15:41:18 【问题描述】:

我在 C# 中有一个异步函数如下:

private static async Task AddEmoji(string emoji)

   ...

我有一种情况,我想调用它,但我不想等待它的结果,我不在乎它是否有效。解决这个问题的正确方法是什么?我以为我可以在不添加异步参数的情况下调用它:

AddEmoji("????");

... 可行,但 VS 给了我一个 CS4014 编译警告,所以我想这是不对的。然后我尝试为它创建一个任务:

Task.Run(() => await AddEmoji("????"));

但这给了我一个实际的编译错误:

错误 CS4034 'await' 运算符只能在异步中使用 拉姆达表达式。考虑用 'async' 修饰符。

解决这个问题的正确方法是什么?

【问题讨论】:

你试过不等待就调用它吗:Task.Run(() => AddEmoji("????")); 是第二次尝试 - 你可能需要 Task.Run(async () => await AddEmoji("????")); - 但是,你可能只是可以将任务放在地板上...... GC.KeepAlive(somethingYouDoNotCareAbout) 在大多数情况下效果很好(调用 @ 987654326@ 实际上只是一个不透明的方法,什么都不做)或者只是抑制警告! 如果您不在乎它是否有效,为什么还要首先调用它? 大家好 - 如果我调用 @NikolaBabic 或 Marc 的建议,我会收到 CS4014(考虑应用“等待”运算符)警告。并不是说我完全不在乎它是否有效——我只是不想等待它返回或知道它是否有效,因为如果它失败了,我不会特别做不同的事情。跨度> 请注意,在 .NET 4.5 之前将任务放在地板上是一个非常糟糕的主意。如果任务包含异常,任务的终结器将重新抛出它。这在 4.5 中已更改。 【参考方案1】:

在您的 Task.Run() 中,等待不会进入 Func。您通常会等待任务本身(将await 放在Task.Run() 之前),但我认为在您的情况下,您可以省略等待以便不等待并忽略结果:

Task.Run(() => AddEmjoi("?"))

【讨论】:

这抓取了一个新的 ThreadPool 线程只是为了运行AddEmoji 的第一位,这是不必要的昂贵 如果我这样做,我会得到一个 CS4014(考虑应用“await”运算符)。它有效,但显然这不是最佳实践,因为我收到了警告。所以我想知道“正确”的做法是什么。我也同意@canton7 - 似乎在这里使用 Task.Run 只是创建了一个我没有使用的额外层。【参考方案2】:

我不想等待它的结果,我不在乎它是否有效。

这是非常不寻常的;首先要检查的是这个假设,您实际上不需要知道它何时完成(或 if 它完成),并且您可以默默地忽略所有异常。这也意味着当您的应用程序退出时,您可以接受这项工作永远不会完成(同样,您不会知道,因为异常被忽略) - 请记住,在 ASP.NET 等托管场景中,您 不要控制您的应用何时退出。

也就是说,你可以这样“一劳永逸”:

var _ = AddEmoji("?");

【讨论】:

我经常遇到这种情况。考虑 1) 一种从流中异步读取数据的方法,在无限循环中,发布它读取的帧。此方法永远不会返回,但我希望我的应用程序在抛出意外异常时崩溃; 2)一个System.Threading.Timer回调,做异步操作,有自己的错误处理; 3) 状态机的状态入口处理程序,它执行一些异步工作,并在工作成功或失败时在状态机上触发适当的事件。对于这些情况,我还没有找到比 async void 更好的方法。 @canton7:第一个例子是不是“一劳永逸”,因为你想要检测错误。第二个示例是 async void 的一个很好的用例 - 请注意,它是一个 事件,这正是 async void 首先被添加到语言中的原因。第三个例子更可疑。在我看来,一个更好的设计可能是可能的,但即使在那个例子中,你在谈论一个“处理程序”——即一个逻辑事件,即使它不是一个字面事件,所以async void在那里是可以接受的,也是。 感谢您的回复。在“永远不应该使用async void”之后,很难找到解决这些情况的好方法。在第一个示例中,包含无限循环的方法通常有自己的错误处理,但未处理的异常应该会导致应用程序崩溃(例如git.io/fje5z)。在那种情况下,async void 也很合适,对吧?由于在捕获的 SynchronizationContext / ThreadPool 上重新抛出了未处理的异常,因此具有预期的效果。同意2的解释,还在寻找3上更好的设计! @canton7:我是“避免async void”咒语的鼻祖。正如我的original article on the subject 中所述,指导方针是避免async void除非您将它用于事件处理程序(或类似事件的语义)。所以我并不是说它应该永远被使用——对于async新手来说,当他们打算使用async Task时使用async void只是一个常见的错误。【参考方案3】:

这不适用于 asp.net -- async void 方法不允许在 asp.net 应用程序中使用。不过,控制台、WPF、Winforms 等应用程序都很好。请参阅 cmets 了解完整讨论。

最好的办法是在 async void 方法中 await 它。

只写AddEmoji("?"); 的问题是:如果它失败了会发生什么?如果在AddEmoji 中引发异常,则该异常将捆绑在Task 中,AddEmoji 返回。但是,因为您将 Task 扔掉了,所以您永远不会知道。所以AddEmoji 可能会以一种糟糕的方式失败,但你永远不会知道。这就是编译器给你一个错误的原因。

但是,async void 方法可以避免这种情况。如果在 async void 方法中引发异常,则该异常会在 ThreadPool 上重新引发,这将导致您的应用程序崩溃。你肯定知道出了什么问题!

人们建议不要使用async void 方法,但它们非常适合即发即弃的情况。该建议源于人们在确实想知道操作何时完成时尝试使用async void 方法。

public async void AddEmojiImpl(string emoji) => await AddEmoji(emoji);

或者

public async void AddEmojiImpl(string emoji)

    try 
    
        await AddEmojiImpl(emoji);
    
    catch (EmojiAddException e)
    
        // ...
    

本地函数对此特别有用。

【讨论】:

“但它们非常适合即发即弃的情况。” - 不,我强烈反对 - 这是一个非常糟糕的事情原因 是:有些框架(包括 MVC)主动阻止 - 意思是:它会导致异常(由于同步上下文选择不允许它)。我在语义上同意你的观点,但实际上养成做事的习惯真的很糟糕。如果它今天不咬你,它可能会在以后由于内部框架的变化而意外地开始失败。由于正是这样做,我不得不重新发行库。 @MarcGravell 请详细说明您所说的“主动阻止”是什么意思?您的意思是捕获的 SynchronizationContext 不允许将继续发布到它吗?这不是我遇到过的——你有这方面的资料吗?当然,在您在 cmets 中提倡的“只需调用它并使用 GC.KeepAlive 来抑制警告”方法中存在完全相同的风险,因为 AddEmoji 可以随时回发到 SynchronizationContext? 通过“主动阻止”,我的意思是:当你这样做时,它实际上会抛出异常;同步上下文执行此操作 - ASP.NET 的错误是 i.stack.imgur.com/uad2C.png - 这仅在 调用 async void 方法时发生 - 引发 from here, specifically(当 _state.AllowVoidAsyncOperationsfalse)。这样做? 当您使用Task 调用GC.KeepAlive 时(将其放在地板上同时保持编译器满意),它不会触发同步上下文为的async void 场景明确寻找(阻止)-因为它现在是async Task @MarcGravell 有趣。完美,这正是我所追求的。谢谢!这是我看到的反对async void 的第一个真正可靠的论点。我会更新我的答案。

以上是关于如何在.NET中调用异步函数而不等待它[重复]的主要内容,如果未能解决你的问题,请参考以下文章

如何使用回调在任何函数中调用异步等待方法

承诺等待得到解决而不返回

如何在同步函数中等待 JavaScript 中的异步调用?

使用异步而不等待

如何在 ASP.NET 中等待异步 Web 服务调用的结果以获得最佳性能

使用流异步读取文件时如何同步处理每一行/缓冲区[重复]