ChatGPT解决这个技术问题 Extra ChatGPT

异步/等待 - 何时返回任务与无效?

在什么场景下想要使用

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 并且它有效。

I
Ian Kemp

通常,您会想要返回一个任务。主要的例外应该是当您需要具有 void 返回类型(用于事件)时。如果没有理由不允许调用者等待您的任务,为什么不允许呢?返回 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,它们可以确保您的方法在抛出异常时仍然正常工作。

有关详细信息,请参阅:https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming


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

我看到了这篇由 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 事件处理程序时,异步 void 方法在某种程度上是无痛的,因为异常的处理方式与非异步方法中的处理方式相同;它们被扔到 Dispatcher 上。有可能从此类异常中恢复,对于大多数情况来说是正确的。然而,在 UI 事件处理程序之外,异步 void 方法使用起来有些危险,而且可能不容易找到。


那是对的吗?我认为异步方法中的异常是在 Task 中捕获的。而且 afaik 总是有一个默认的 SynchronizationContext
P
Pang

调用 async void 的问题在于

你甚至没有拿回任务。您无法知道函数的任务何时完成。 —— async 和 await 速成课程 |旧事新事

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

异步任务 SomethingAsync() { ... return t; } async Task SomethingAsync() { ... } async void SomethingAsync() { ... } 在所有情况下,函数都会转换为任务链。不同之处在于函数返回的内容。在第一种情况下,函数返回一个最终产生 t 的任务。在第二种情况下,该函数返回一个没有产品的任务,但您仍然可以等待它以了解它何时运行完成。第三种情况是令人讨厌的。第三种情况和第二种情况一样,只是你连任务都拿不回来。您无法知道函数的任务何时完成。 async void 情况是“一劳永逸”:您启动任务链,但您不关心它何时完成。当函数返回时,您所知道的就是直到第一次等待为止的所有内容都已执行。第一次等待之后的所有内容都将在未来某个您无法访问的未指定点运行。


b
bboyle1234

我认为您也可以使用 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
@SiZi 捕获在示例中所示的 async void 方法内,它被捕获。
如果您的要求更改为不需要做任何工作,如果您无法记录它,那该怎么办?那么您必须更改整个 API 中的签名,而不是仅仅更改当前具有 // Ignore Exception 的逻辑
C
Community

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
D
David Klempfner

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

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

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


用于事件处理程序的 void 异步方法。所以有必要在你不想等待完成的地方使用异步。