异步/等待-什么时候返回Task vs void?


502

在什么情况下您想使用

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));
}

20
请注意,异步方法应始终以名称Async作为后缀。Example Foo()将变为FooAsync()
弗雷德(Fred)

30
@Fred通常,但并非总是如此。这只是约定,基于事件的类或接口协定适用于该约定,请参见MSDN。例如,您不应重命名常见事件处理程序,例如Button1_Click。

14
只是说明你不应该用Thread.Sleep你的任务,你应该await Task.Delay(num)代替
鲍勃淡水河谷

45
@fred我不同意这一点,仅当您同时提供带有同步和异步选项的接口时,才应使用IMO添加异步后缀的方法。当只有一个意图时,用蓝精灵用异步命名事物是没有意义的。举例来说,Task.Delay并不是Task.AsyncDelay所有的任务方法都是异步的
不喜欢

11
我今天早上有一个有趣的问题,以2的WebAPI控制器的方法,它被宣布为async void代替async Task。该方法崩溃是因为该方法使用的是声明为控制器成员的Entity Framework上下文对象,该对象在该方法完成执行之前已处理掉。框架在其方法完成执行之前就已处置了控制器。我将方法更改为异步任务,它可以工作。
哥斯达黎加

Answers:


417

1)通常,您需要返回Task。主要的例外应该是当您需要具有void返回类型(用于事件)时。如果没有理由禁止来电者await您的任务,为什么要禁止它呢?

2)async返回的方法void在另一方面是特殊的:它们代表顶级异步操作,并且在您的任务返回异常时会起作用。最简单的方法是通过一个示例来显示差异:

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在这里,它们可以确保如果引发异常,您的方法仍然可以正常工作。

有关更多信息,请参见:http : //msdn.microsoft.com/zh-cn/magazine/jj991977.aspx


10
我的意思f不是g在评论中。来自的异常f传递到SynchronizationContextg将引发UnobservedTaskException,但UTE如果不处理,将不再使进程崩溃。在某些情况下,可以这样忽略“异步异常”。
Stephen Cleary 2012年

3
如果具有WhenAny多个,则Task导致异常。您通常只需要处理第一个,而您常常想忽略其他。
史蒂芬·克利西

1
@StephenCleary谢谢,我想这是一个很好的例子,尽管这取决于您WhenAny首先调用的原因是否可以忽略其他异常:我拥有的主要用例仍然会在任何完成时等待其余任务,无论有没有例外。

10
关于您为什么建议返回Task而不是void的问题,我有些困惑。就像您说的那样,f()会抛出异常,而g()不会。是否最好了解这些后台线程异常?
user981225

2
@ user981225的确如此,但这随后成为g的调用者的责任:调用g的每个方法都应该异步并也使用await。这是一个准则,而不是硬性规定,您可以决定在您的特定程序中,让g return void更容易。

40

我也碰到过这种关于非常有用的文章asyncvoid杰罗姆拉班写着: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

最重要的是,这async+void可能会使系统崩溃,通常只应在UI端事件处理程序上使用。

其背后的原因是AsyncVoidMethodBuilder使用的“同步上下文”,在此示例中没有。当没有环境同步上下文时,异步void方法的主体无法处理的任何异常都会在ThreadPool上重新抛出。尽管似乎没有其他逻辑上的地方可以抛出这种未处理的异常,但是不幸的结果是该进程正在终止,因为自.NET 2.0起,ThreadPool上的未处理的异常有效地终止了该进程。您可以使用AppDomain.UnhandledException事件来拦截所有未处理的异常,但是无法从此事件中恢复进程。

在编写UI事件处理程序时,异步void方法可以轻松实现,因为对异常的处理方式与非异步方法相同。他们扔在调度程序上。有可能从此类异常中恢复,对于大多数情况而言,这种恢复是正确的。但是,在UI事件处理程序之外,异步void方法在某种程度上使用起来很危险,而且可能不那么容易找到。


那是对的吗?我认为异步方法内部的异常是在Task中捕获的。同样,afaik始终存在默认的SynchronizationContext
NM

30

我从这句话中得到了清晰的主意。

  1. 异步void方法具有不同的错误处理语义。从异步Task或异步Task方法抛出异常时,将捕获该异常并将其放置在Task对象上。使用异步void方法时,没有Task对象,因此从异步void方法抛出的任何异常都将直接在SynchronizationContext(SynchronizationContext表示可能在其中执行代码的位置)上引发。开始了

异步无效方法的异常无法捕获

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

可以使用AppDomain.UnhandledException或类似的GUI / ASP.NET应用程序的全部捕获事件来观察这些异常,但是使用这些事件进行常规异常处理是无法维护的秘诀(会使应用程序崩溃)。

  1. 异步void方法具有不同的构成语义。返回任务或任务的异步方法可以使用await,Task.WhenAny,Task.WhenAll等轻松组成。返回void的异步方法不能提供一种简单的方法来通知调用代码它们已完成。启动多个异步void方法很容易,但是确定它们何时完成并不容易。异步void方法在启动和完成时会通知其SynchronizationContext,但是自定义SynchronizationContext是常规应用程序代码的复杂解决方案。

  2. 使用同步事件处理程序时,Async Void方法很有用,因为它们直接在SynchronizationContext上引发异常,这与同步事件处理程序的行为类似

有关更多详细信息,请检查此链接 https://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx


21

调用异步void的问题是您什至没有收回任务,也无法知道函数的任务何时完成(请参阅https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ?p = 96655

这是调用异步函数的三种方法:

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

在所有情况下,功能都会转换为一系列任务。区别在于函数返回什么。

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

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

第三种情况是令人讨厌的。第三种情况与第二种情况类似,不同之处在于您甚至没有将任务取回。您无法知道函数任务何时完成。

异步无效情况是一种“即发即弃”:您启动了任务链,但并不关心何时完成。函数返回时,您所知道的就是直到第一次等待的所有操作都已执行。第一次等待后的所有内容将在将来无法访问的某个未指定的时间运行。


6

我认为您也可以使用它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());
        }
    }
}

1
调用异步void的问题在于,您甚至没有收回任务,也无法知道函数的任务何时完成
user8128167

这对于(稀有的)后台操作确实很有意义,在这些后台操作中,您不需要关心结果,也不想让异常影响其他操作。典型的用例是将日志事件发送到日志服务器。您希望这种情况在后台发生,并且您不希望服务/请求处理程序在日志服务器关闭时失败。当然,您必须捕获所有异常,否则您的过程将被终止。还是有更好的方法来实现这一目标?
弗洛里安(Florian Winter)

异步无效方法的异常无法捕获。msdn.microsoft.com/en-us/magazine/jj991977.aspx
子字

3
@SiZi捕获位于示例示例中所示的异步void方法内部,并且已捕获。
bboyle1234 '18 -10-16

当您的需求更改为无法登录就不进行任何操作时该怎么办-那么您必须更改整个API中的签名,而不是仅更改当前具有//忽略异常的逻辑
Milney

0

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

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

因此,如果该方法是异步的,则最好等待它,因为您可以失去异步优势。


-2

根据Microsoft文档,切勿使用async void

不要这样做:下面的示例使用async void它在到达第一次等待时使HTTP请求完成:

  • 在ASP.NET Core应用程序中,这总是一个坏习惯。

  • HTTP请求完成后访问HttpResponse。

  • 导致进程崩溃。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.