【问题标题】:async/await - when to return a Task vs void?异步/等待 - 何时返回任务与无效?
【发布时间】:2012-08-22 01:33:21
【问题描述】:

想在什么场景下使用

public async Task AsyncMethod(int num)

而不是

public async void AsyncMethod(int num)

我能想到的唯一场景是您是否需要任务能够跟踪其进度。

另外,在下面的方法中,async和await关键字是不是不必要的?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

【问题讨论】:

  • 请注意,异步方法应始终以名称 Async 为后缀。示例Foo() 将变为FooAsync()
  • @Fred 大多数情况下,但并非总是如此。这只是约定,该约定的公认例外是基于事件的类或接口契约see MSDN。例如,您不应重命名常见的事件处理程序,例如 Button1_Click。
  • 请注意,您不应该在任务中使用Thread.Sleep,而应该使用await Task.Delay(num)
  • @fred 我不同意这一点,IMO 添加异步后缀仅应在您提供具有同步和异步选项的接口时使用。当只有一个意图时,Smurf 用异步命名事物是没有意义的。例如Task.Delay 不是Task.AsyncDelay,因为任务上的所有方法都是异步的
  • 今天早上我遇到了一个关于 webapi 2 控制器方法的有趣问题,它被声明为 async void 而不是 async Task。该方法崩溃是因为它使用了一个声明为控制器成员的实体框架上下文对象,该对象在方法完成执行之前被释放。框架在其方法完成执行之前就将控制器配置好了。我将方法更改为 async Task 并且它起作用了。

标签: c# asynchronous .net-4.5


【解决方案1】:
  1. 通常,您希望返回 Task。主要的例外应该是当您需要有一个void 返回类型(用于事件)。如果没有理由禁止调用者 await 您的任务,为什么要禁止它?

  2. 返回voidasync 方法在另一个方面是特殊的:它们代表顶级异步操作,并且当您的任务返回异常时,它们具有额外的规则。最简单的方法是通过示例来显示差异:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

f 的异常总是被“观察到”。离开顶级异步方法的异常被简单地视为任何其他未处理的异常。从未观察到g 的异常。当垃圾收集器来清理任务时,它看到任务导致异常,并且没有人处理异常。发生这种情况时,TaskScheduler.UnobservedTaskException 处理程序将运行。你不应该让这种情况发生。要使用您的示例,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

是的,在这里使用asyncawait,它们可以确保您的方法在抛出异常时仍然正常工作。

欲了解更多信息,请参阅:https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

【讨论】:

  • 我在评论中指的是f,而不是gf 的异常被传递给SynchronizationContextg 将引发 UnobservedTaskException,但 UTE 如果未处理,则不再使进程崩溃。在某些情况下,像这样被忽略的“异步异常”是可以接受的。
  • 如果您有 WhenAny 和多个 Tasks 会导致异常。您通常只需要处理第一个,而您通常想忽略其他的。
  • @StephenCleary 谢谢,我想这是一个很好的例子,虽然这取决于你首先调用WhenAny 的原因是否可以忽略其他异常:我的主要用例当任何任务完成时,它仍然会等待剩余的任务,无论是否有异常。
  • 我有点困惑,为什么您建议返回任务而不是 void。就像你说的, f() 会抛出异常,但 g() 不会。了解这些后台线程异常不是最好吗?
  • @user981225 确实如此,但这成为 g 调用者的责任:每个调用 g 的方法都应该是异步的并且也应该使用 await。这是一个指导原则,而不是硬性规则,您可以决定在您的特定程序中,让 g return void 更容易。
【解决方案2】:

我看到了这篇由 Jérôme Laban 撰写的关于 asyncvoid 的非常有用的文章: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.html

底线是async+void 会导致系统崩溃,通常只能在 UI 端事件处理程序中使用。

这背后的原因是 AsyncVoidMethodBuilder,在这个例子中没有。没有的时候 环境同步上下文,任何未处理的异常 async void 方法的主体在 ThreadPool 上重新抛出。尽管 似乎没有其他合乎逻辑的地方可以处理这种未处理的 可能会抛出异常,不幸的是该过程 正在被终止,因为 ThreadPool 上有未处理的异常 从 .NET 2.0 开始有效地终止进程。你可以拦截 使用 AppDomain.UnhandledException 事件的所有未处理异常, 但是没有办法从这个事件中恢复进程。

在编写 UI 事件处理程序时,async void 方法在某种程度上是 无痛,因为异常的处理方式与 非异步方法;它们被扔到 Dispatcher 上。有一个 从此类异常中恢复的可能性,这不仅仅是正确的 对于大多数情况。然而,在 UI 事件处理程序之外,异步无效 方法使用起来有些危险,而且可能不太容易找到。

【讨论】:

  • 正确吗?我认为异步方法中的异常是在 Task 中捕获的。而且 afaik 总是有一个默认的 SynchronizationContext
【解决方案3】:

调用 async void 的问题在于

你甚至没有拿回任务。您无法知道函数的任务何时完成。 ——Crash course in async and await | The Old New Thing

以下是调用异步函数的三种方式:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

在所有情况下,函数都被转换为一系列任务。不同之处在于函数返回的内容。

在第一种情况下,函数返回一个最终产生 t 的任务。

在第二种情况下,函数返回一个没有产品的任务,但是你可以 仍在等待它知道它何时运行完成。

第三种情况很糟糕。第三种情况和第二种情况一样,除了 你甚至没有拿回任务。你无法知道什么时候 该函数的任务已完成。

异步 ​​void 情况是“火灾和 忘记”:你启动了任务链,但你不在乎它是什么时候开始的 完成的。当函数返回时,你只知道一切 直到第一个等待已执行。第一次等待之后的一切 将在未来某个未指定的时间点运行,您没有 访问。

【讨论】:

    【解决方案4】:

    我认为你也可以使用async void 来启动后台操作,只要你小心捕捉异常。想法?

    class Program {
    
        static bool isFinished = false;
    
        static void Main(string[] args) {
    
            // Kick off the background operation and don't care about when it completes
            BackgroundWork();
    
            Console.WriteLine("Press enter when you're ready to stop the background operation.");
            Console.ReadLine();
            isFinished = true;
        }
    
        // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
        static async void BackgroundWork() {
            // It's important to catch exceptions so we don't crash the appliation.
            try {
                // This operation will end after ten interations or when the app closes. Whichever happens first.
                for (var count = 1; count <= 10 && !isFinished; count++) {
                    await Task.Delay(1000);
                    Console.WriteLine($"{count} seconds of work elapsed.");
                }
                Console.WriteLine("Background operation came to an end.");
            } catch (Exception x) {
                Console.WriteLine("Caught exception:");
                Console.WriteLine(x.ToString());
            }
        }
    }
    

    【讨论】:

    • 调用 async void 的问题是你连任务都拿不回来,你无法知道函数的任务什么时候完成
    • 这对于(罕见的)后台操作确实有意义,您不关心结果并且您不希望异常影响其他操作。一个典型的用例是将日志事件发送到日志服务器。您希望这在后台发生,并且您不希望您的服务/请求处理程序在日志服务器关闭时失败。当然,您必须捕获所有异常,否则您的进程将被终止。还是有更好的方法来实现这一点?
    • 不能用 Catch 捕获来自异步 Void 方法的异常。 msdn.microsoft.com/en-us/magazine/jj991977.aspx
    • 如果您的需求更改为不需要做任何工作,如果您无法记录它,那么您必须更改整个 API 中的签名,而不是仅仅更改当前具有的逻辑 // Ignore Exception
    【解决方案5】:

    According to Microsoft documentation,永远不要使用async void

    不要这样做:以下示例使用 async void 到达第一个等待时,HTTP 请求完成:

    • 这在 ASP.NET Core 应用程序中总是不好的做法。

    • 在 HTTP 请求完成后访问 HttpResponse。

    • 使进程崩溃。

    【讨论】:

    • 这个文档示例的重点实际上不是关于async void,而是关于在请求完成后使用http上下文。他们通过使用返回 void 的错误异步操作来导致此问题,禁止管道在它之后等待。所以这不是关于async void 的一般性建议,而只是它可能产生的不良后果的一个示例。
    • 建议不要使用async void的实际Microsoft文档是here。正如其他人已经链接的那样,还有这个msdn blog
    【解决方案6】:

    我的答案很简单 你不能等待 void 方法

    Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs
    

    所以如果方法是async,最好是awaitable,因为你会失去async的优势。

    【讨论】:

      猜你喜欢
      • 2014-10-01
      • 2023-03-24
      • 2016-10-27
      • 1970-01-01
      • 1970-01-01
      • 2014-03-18
      • 2017-12-06
      相关资源
      最近更新 更多