ChatGPT解决这个技术问题 Extra ChatGPT

如何在不等待的情况下在 C# 中安全地调用异步方法

我有一个不返回数据的 async 方法:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

我从另一个返回一些数据的方法调用它:

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

不等待就调用 MyAsyncMethod() 会导致 Visual Studio 中出现“Because this call is not awaited, the current method continues to run before the call is completed”警告。在该警告的页面上,它指出:

仅当您确定不想等待异步调用完成并且被调用的方法不会引发任何异常时,才应考虑取消警告。

我确定我不想等待通话完成;我不需要也没有时间。但是调用可能会引发异常。

我偶然发现了这个问题几次,我确信这是一个常见的问题,必须有一个共同的解决方案。

如何在不等待结果的情况下安全地调用异步方法?

更新:

对于建议我等待结果的人来说,这是响应我们 Web 服务 (ASP.NET Web API) 上的 Web 请求的代码。在 UI 上下文中等待使 UI 线程保持空闲,但在 Web 请求调用中等待将等待任务完成后再响应请求,从而无缘无故地增加响应时间。

如果您不想等待结果,唯一的选择是忽略/抑制警告。如果您确实想要等待结果/异常,那么 MyAsyncMethod().Wait()
关于您的编辑:这对我来说没有意义。假设响应在请求后 1 秒发送到客户端,2 秒后您的异步方法抛出异常。你会怎么处理这个例外?如果您的响应已发送,则无法将其发送给客户端。你还会用它做什么?
@Romoku 够公平的。无论如何,假设有人查看日志。 :)
ASP.NET Web API 场景的一个变体是一个长寿命进程(例如,Windows 服务)中的自托管 Web API,其中一个请求创建了一个冗长的后台任务来执行一些昂贵的任务,但仍然想要使用 HTTP 202(已接受)快速获得响应。
为什么不使用 Task.Run()

C
Community

如果你想“异步”获取异常,你可以这样做:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

这将允许您处理“主”线程以外的线程上的异常。这意味着您不必“等待”来自调用 MyAsyncMethod 的线程对 MyAsyncMethod() 的调用;但是,仍然允许您对异常执行某些操作——但前提是发生异常。

更新:

从技术上讲,您可以使用 await 做类似的事情:

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

...如果您需要专门使用 try/catch(或 using),这将很有用,但我发现 ContinueWith 更明确一点,因为您必须知道 ConfigureAwait(false) 的含义。


我把它变成了 Task 上的扩展方法: public static class AsyncUtility { public static void PerformAsyncTaskWithoutAwait(this Task task, Action exceptionHandler) { var dummy = task.ContinueWith(t => exceptionHandler(t), TaskContinuationOptions .OnlyOnFaulted); } } 用法:MyAsyncMethod().PerformAsyncTaskWithoutAwait(t => log.ErrorFormat("调用 MyAsyncMethod 时发生错误:\n{0}", t.Exception));
否决者。注释?如果答案有问题,我很想知道和/或修复。
嘿 - 我没有投反对票,但是......你能解释一下你的更新吗?呼叫不应该被等待,所以如果你这样做,那么你确实等待呼叫,你只是不要继续捕获的上下文......
ContinueWith 版本与 try{ await }catch{} 版本不同。在第一个版本中,ContinueWith() 之后的所有内容都会立即执行。最初的任务被解雇并被遗忘。在第二个版本中,catch{} 之后的所有内容只有在初始任务完成后才会执行。第二个版本相当于“await MyAsyncMethod().ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted).ConfigureAwait(fals);
确认来自 Thanasis Ioannidis 的评论最适合回答 OP 问题。 @PeterRitchie,我强烈建议您更新您接受的答案,以免被评论淹没。
S
Stephen Cleary

您应该首先考虑将 GetStringData 设为 async 方法,并使其 await 成为从 MyAsyncMethod 返回的任务。

如果您绝对确定不需要处理来自 MyAsyncMethod 的异常,知道它何时完成,那么您可以这样做:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

顺便说一句,这不是“常见问题”。很少有人想要执行一些代码而不关心它是否完成而不关心它是否成功完成。

更新:

由于您使用 ASP.NET 并希望早点返回,您可以找到我的 blog post on the subject useful。但是,ASP.NET 并不是为此而设计的,并且没有保证您的代码将在返回响应后运行。 ASP.NET 会尽最大努力让它运行,但不能保证。

所以,对于简单的事情来说,这是一个很好的解决方案,比如将一个事件扔到一个日志中,如果你在这里和那里丢失一些事件并不重要。对于任何类型的关键业务操作来说,这都不是一个好的解决方案。在这些情况下,您必须采用更复杂的体系结构,以持久的方式保存操作(例如,Azure 队列、MSMQ)和单独的后台进程(例如,Azure Worker Role、Win32 服务)来处理它们。


我想你可能误会了。我确实关心它是否会引发异常并失败,但我不想在返回数据之前等待该方法。如果这有什么不同,还请参阅我对我正在工作的上下文的编辑。
@GeorgePowell:在没有主动请求的情况下在 ASP.NET 上下文中运行代码是非常危险的。我有一个blog post that may help you out,但在不了解您的问题的情况下,我不能说我是否会推荐这种方法。
@StephenCleary 我也有类似的需求。在我的示例中,我有/需要一个批处理引擎在云中运行,我将“ping”端点以启动批处理,但我想立即返回。由于 ping 它开始,它可以从那里处理所有事情。如果抛出异常,那么它们只会记录在我的“BatchProcessLog/Error”表中......
在 C#7 中,您可以将 var _ = MyAsyncMethod(); 替换为 _ = MyAsyncMethod();。这仍然避免了警告 CS4014,但它更明确地表明您没有使用该变量。
我认为 OP 的意思是他不希望客户端(HTTP 请求)等待将某些内容记录到数据库是否成功。如果日志记录失败,确保应用程序仍然可以完全控制处理该异常,但我们不需要客户端等待处理该异常。我认为这意味着需要在后台线程上完成工作。是的.. 保留一个线程来执行异步任务很糟糕,但是需要这样做来处理潜在的异常。
G
George Powell

Peter Ritchie 的回答正是我想要的,Stephen Cleary's article 关于尽早返回 ASP.NET 非常有帮助。

然而,作为一个更普遍的问题(不是特定于 ASP.NET 上下文),以下控制台应用程序使用 Task.ContinueWith(...) 演示了彼得的答案的用法和行为

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData() 在不等待 MyAsyncMethod() 的情况下提前返回,并且在 MyAsyncMethod() 中抛出的异常在 OnMyAsyncMethodFailed(Task task) 中处理,而 notGetStringData() 周围的 try/catch 中处理


删除 Console.ReadLine(); 并在 MyAsyncMethod 中添加一点睡眠/延迟,您将永远不会看到异常。
F
Filimindji

我最终得到了这个解决方案:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}

w
wast

这称为“一劳永逸”,为此有一个 extension

消耗一个任务并且不做任何事情。对于异步方法中对异步方法的即发即弃调用很有用。

安装 nuget package

利用:

MyAsyncMethod().Forget();

编辑:有 another way 我最近一直在使用:

_ = MyAsyncMethod();

它什么也没做,只是消除警告的一个技巧。请参阅github.com/microsoft/vs-threading/blob/master/src/…
更正:它做了很多事情来避免对所拨打的电话做出反应。
C
CharithJ

不是最佳实践,您应该尝试避免这种情况。

但是,为了解决“在没有等待的情况下在 C# 中调用异步方法”的问题,您可以在 Task.Run 中执行异步方法。这种方法将等到 MyAsyncMethod 完成。

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

await 异步解开任务的 Result,而仅使用 Result 会阻塞,直到任务完成。

如果你想把它包装在一个辅助类中:

public static class AsyncHelper
{
    public static void Sync(Func<Task> func) => Task.Run(func).ConfigureAwait(false);

    public static T Sync<T>(Func<Task<T>> func) => Task.Run(func).Result;

}

并打电话给

public string GetStringData()
{
    AsyncHelper.Sync(() => MyAsyncMethod());
    return "hello world";
}

MyAsyncMethod().Result 不会做同样的事情吗?
A
Adam Diament

我在这里聚会迟到了,但是我一直在使用一个很棒的库,我在其他答案中没有看到它

https://github.com/brminnick/AsyncAwaitBestPractices

如果你需要“Fire And Forget”,你可以调用任务的扩展方法。

将操作 onException 传递给调用可确保您获得两全其美 - 无需等待执行并减慢用户速度,同时保留以优雅方式处理异常的能力。

在您的示例中,您将像这样使用它:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

它还提供了可等待的 AsyncCommands 实现 ICommand 开箱即用,这对我的 MVVM Xamarin 解决方案非常有用


D
Drew Noakes

我想问题出现了,你为什么需要这样做?在 C# 5.0 中使用 async 的原因是您可以等待结果。这个方法实际上并不是异步的,只是简单的一次调用,以免过多干扰当前线程。

也许启动一个线程并让它自己完成可能会更好。


async 不仅仅是“等待”结果。 “await”意味着“await”之后的行在调用“await”的同一线程上异步执行。当然,这可以在没有“等待”的情况下完成,但是您最终会拥有一堆委托并失去代码的顺序外观(以及使用 usingtry/catch.. .
@PeterRitchie您可以拥有一个功能异步的方法,不使用 await 关键字,也不使用 async 关键字,但是使用 async 关键字而不使用 { 1} 在该方法的定义中。
@PeterRitchie 从声明中:“async 不仅仅是“等待”结果。” async 关键字(您通过将其括在反引号中来暗示它是关键字)意味着什么都没有,只不过是在等待结果。作为一般的 CS 概念,它是异步的,这意味着不仅仅是等待结果。
@Servy async 创建一个状态机来管理异步方法中的任何等待。如果方法中没有 await,它仍会创建该状态机——但该方法不是异步的。如果 async 方法返回 void,则无需等待。因此,它更多而不仅仅是等待结果。
@PeterRitchie 只要该方法返回一个任务,您就可以等待它。不需要状态机或 async 关键字。您需要做的所有事情(以及在这种特殊情况下最终使用状态机真正发生的事情)是该方法同步运行,然后包装在一个已完成的任务中。我想从技术上讲,您不只是删除状态机;您删除状态机,然后调用 Task.FromResult。我假设您(以及编译器编写者)可以自己添加附录。
h
haimb

在具有消息循环的技术上(不确定 ASP 是否是其中之一),您可以阻塞循环并处理消息,直到任务结束,并使用 ContinueWith 解除对代码的阻塞:

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

这种方法类似于阻止 ShowDialog 并仍然保持 UI 响应。


T
TarmoPikaro

通常异步方法返回 Task 类。如果您使用 Wait() 方法或 Result 属性并且代码抛出异常 - 异常类型被包装到 AggregateException - 那么您需要查询 Exception.InnerException 以找到正确的异常。

但也可以改用 .GetAwaiter().GetResult() - 它也会等待异步任务,但不会包装异常。

所以这里是一个简短的例子:

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

您可能还希望能够从异步函数返回一些参数 - 这可以通过在异步函数中提供额外的 Action<return type> 来实现,例如:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

请注意,异步方法通常具有 ASync 后缀命名,只是为了能够避免同名同步函数之间的冲突。 (例如 FileStream.ReadAsync)- 我已更新函数名称以遵循此建议。


M
Miguel

也许我太天真了,但是,您不能创建一个在调用 GetStringData() 时引发的事件并附加一个调用并等待异步方法的 EventHandler 吗?

就像是:

public event EventHandler FireAsync;

public string GetStringData()
{
   FireAsync?.Invoke(this, EventArgs.Empty);
   return "hello world";
}

public async void HandleFireAsync(object sender, EventArgs e)
{
   await MyAsyncMethod();
}

在代码中的某处附加和分离事件:

FireAsync += HandleFireAsync;

(...)

FireAsync -= HandleFireAsync;

不确定这是否可能是反模式(如果是,请告诉我),但它会捕获异常并从 GetStringData() 快速返回。


这只是将异步方法转换为 async void 的一种过于复杂的方式,因此行为从即发即弃更改为即发即崩溃。您可以通过如下简单的方式实现相同的目的:async void OnErrorCrash(this Task task) => await task;
V
Venkatesh Muniyandi

这很简单,只需调用 asyncMethod().Result 即可调用而无需等待。下面是示例代码,here 是小提琴

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
                    
public class Program
{
    public static void Main()
    {   
        var asyncDemo = new AsyncDemo();
        asyncDemo.TestMethod1Void().Wait();
        var result = asyncDemo.TestMethod1().Result;
        Console.WriteLine(result);
    }
}

public class AsyncDemo {
    
    public async Task<string> TestMethod1()
    {
        Thread.Sleep(1000);
        return  "From Async Method";        
    }
    
    public async Task TestMethod1Void()
    {
        Thread.Sleep(1000);
        Console.WriteLine("Async Void Method");     
    }
}

E
Ernesto Garcia

解决方案是将 HttpClient 启动到另一个没有 sincronization 上下文的执行任务中:

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);