【问题标题】:How to safely call an async method in C# without await如何在不等待的情况下在 C# 中安全地调用异步方法
【发布时间】:2013-03-09 12:13:18
【问题描述】:

我有一个不返回数据的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”警告。在该警告的页面上,它指出:

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

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

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

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

更新:

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

【问题讨论】:

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

标签: c# exception async-await task task-parallel-library


【解决方案1】:

不是最佳做法,您应该尽量避免这样做。

但是,为了解决“在 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";
}

【讨论】:

    【解决方案2】:

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

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

    安装nuget package

    用途:

    MyAsyncMethod().Forget();
    

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

    _ = MyAsyncMethod();
    

    【讨论】:

    【解决方案3】:

    也许我太天真了,但是,你不能创建一个在调用 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) =&gt; await task;
    【解决方案4】:

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

    https://github.com/brminnick/AsyncAwaitBestPractices

    如果你需要“一劳永逸”,你可以调用任务的扩展方法。

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

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

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

    它还为我的 MVVM Xamarin 解决方案提供了实现 ICommand 的可等待 AsyncCommands

    【讨论】:

      【解决方案5】:

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

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

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

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

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

      public string GetStringData()
      {
          return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
      }
      
      public async Task<String> MyMethodWithReturnParameterAsync()
      {
          return "test";
      }
      

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

      【讨论】:

        【解决方案6】:

        解决方案是启动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);
        

        【讨论】:

          【解决方案7】:

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

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

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

          【讨论】:

            【解决方案8】:

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

              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 更明确一点,因为您必须知道@987654321 是什么@ 表示。

            【讨论】:

            • 我在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("An error occurred while calling 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,我强烈建议您更新您接受的答案,以避免这被埋没在 cmets 中。
            【解决方案9】:

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

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

            【讨论】:

              【解决方案10】:

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

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

              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) 中处理,而nottry/catch 周围GetStringData()

              【讨论】:

              • 删除 Console.ReadLine(); 并在 MyAsyncMethod 中添加一点睡眠/延迟,您将永远不会看到异常。
              【解决方案11】:

              您应该首先考虑将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请求)等待将某些内容记录到数据库是否成功。如果日志记录失败,确保应用程序仍然可以完全控制处理该异常,但我们不需要客户端等待处理该异常。我认为这意味着需要在后台线程上完成工作。是的.. 保留一个线程来执行异步任务很糟糕,但需要这样做以处理潜在的异常。
              【解决方案12】:

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

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

              【讨论】:

              • async 不仅仅是“等待”结果。 “await”意味着“await”之后的行在调用“await”的同一线程上异步执行。当然,这可以在没有“等待”的情况下完成,但是您最终会拥有一堆委托并失去代码的顺序外观(以及使用 usingtry/catch.. .
              • @PeterRitchie 您可以有一个功能异步的方法,不使用 await 关键字,也不使用 async 关键字,但是使用async 关键字在该方法的定义中也没有使用 await
              • @PeterRitchie 从声明中:“async 不仅仅是“等待”结果。” async 关键字(您通过将其括在反引号中暗示它是关键字)意味着 nothing 只是等待结果。与一般的 CS 概念一样,它是异步的,这意味着不仅仅是等待结果。
              • @Servy async 创建一个状态机来管理异步方法中的任何等待。如果方法中没有awaits,它仍然会创建该状态机——但该方法不是异步的。如果async 方法返回void,则无需等待。所以,这更多不仅仅是等待结果。
              • @PeterRitchie 只要该方法返回一个任务,您就可以等待它。不需要状态机或async 关键字。您需要做的所有事情(以及在这种特殊情况下最终使用状态机真正发生的事情)是该方法同步运行,然后包装在一个已完成的任务中。我想从技术上讲,您不只是删除状态机;您删除状态机,然后调用Task.FromResult。我假设您(以及编译器编写者)可以自己添加附录。
              猜你喜欢
              • 1970-01-01
              • 2014-09-20
              • 2013-11-16
              • 2019-04-10
              • 1970-01-01
              • 2021-12-19
              • 1970-01-01
              • 2016-07-23
              相关资源
              最近更新 更多