如何在C#中安全地调用异步方法而无需使用await

492

我有一个返回无数据的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中引发"因为此调用没有被等待,所以当前方法在调用完成前继续运行"的警告。在该警告页面上,它指出:
“只有在确信您不想等待异步调用完成且被调用的方法不会引发任何异常时,您才应考虑抑制警告。”
我确定我不需要等待调用完成;我没有时间等待。但是该调用可能会引发异常。
我曾经遇到过这个问题,我相信这是一个常见的问题,必须有一个常见的解决方案。
如何安全地调用异步方法而不等待结果?
更新:
对于建议我只需等待结果的人,这是响应我们Web服务(ASP.NET Web API)上的Web请求的代码。在UI上下文中等待可以保持UI线程空闲,但在Web请求调用中等待Task完成将在响应请求之前等待,从而增加了响应时间,没有理由这样做。

为什么不创建一个完成方法并在那里忽略它呢?因为如果它在后台线程上运行,那么它无论如何都不会阻止程序终止。 - Yahya
2
如果您不想等待结果,唯一的选择是忽略/抑制警告。如果您确实想等待结果/异常,则使用MyAsyncMethod().Wait() - Peter Ritchie
5
关于你的修改:我不理解。假设响应是在请求后1秒钟发送给客户端,而你的异步方法在2秒后抛出异常。你会如何处理这个异常?如果你的响应已经发送给客户端,你不能将异常发送给客户端。那么你还能怎么处理它? - user743382
2
@Romoku 好的,假设有人看日志的话。 :) - user743382
4
一种 ASP.NET Web API 的变体是在一个长时间运行的进程(例如 Windows 服务)中自托管 Web API 场景,其中请求创建一个耗时的后台任务来执行某些昂贵的操作,但仍然希望能够快速收到 HTTP 202(已接受)响应。 - David Rubin
4
为什么不使用Task.Run() - Kyle Delaney
13个回答

289
如果您想“异步地”获取异常,可以执行以下操作:
  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

这将允许你在与“主”线程不同的线程上处理异常。这意味着你不必等待从调用MyAsyncMethod()的线程返回,但是仍然可以处理异常——只有在发生异常时才能这样做。

更新:

技术上,你也可以通过await来实现类似的功能:

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

如果需要明确使用 try/catch(或者using),那么这会很有用,但我认为 ContinueWith 更加明确,因为你必须知道ConfigureAwait(false)的含义。


11
我将其转换为了 Task 的扩展方法: public static class AsyncUtility { public static void PerformAsyncTaskWithoutAwait(this Task task, Action<Task> exceptionHandler) { var dummy = task.ContinueWith(t => exceptionHandler(t), TaskContinuationOptions.OnlyOnFaulted); } }用法: MyAsyncMethod().PerformAsyncTaskWithoutAwait(t => log.ErrorFormat("调用 MyAsyncMethod 时发生错误:\n{0}", t.Exception)); - Mark Avenius
3
留言者。有评论吗?如果答案中有错误,我很乐意知道和/或修复。 - Peter Ritchie
9
嗨 - 我没有点踩,但是...你能解释一下你的更新吗?本来不应该等待那个调用,所以如果你这样做了,那么你就等待那个调用,只是不在捕获的上下文中继续执行。 - Bartosz
51
ContinueWith的版本与try{ await }catch{}的版本不同。在第一个版本中,ContinueWith()之后的所有内容都会立即执行。初始任务将被触发并被忽略。在第二个版本中,catch{}之后的所有内容只有在初始任务完成后才会执行。第二个版本等价于"await MyAsyncMethod().ContinueWith(t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted).ConfigureAwait(false);" - Thanasis Ioannidis
7
确认来自Thanasis Ioannidis的评论最适合回答OP的问题。 @PeterRitchie,我强烈建议您更新您已接受的答案,以避免此评论被埋没。 - jrap
显示剩余6条评论

103

你应该首先考虑将 GetStringData 方法变为 async 方法,并使其使用来自 MyAsyncMethod 返回的任务进行 await

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

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

顺便说一句,这不是一个“常见问题”。想要执行某些代码而不关心它是否完成不关心它是否成功完成非常罕见。

更新:

由于你使用的是ASP.NET并想要尽早返回,您可能会发现我在博客文章上关于此主题的内容很有用。但是,ASP.NET并不是为此而设计的,而且没有保证在返回响应后您的代码将运行。ASP.NET将尽力让它运行,但无法保证它的运行。

因此,对于像将事件放入日志中这样的简单事物,这是一个不错的解决方案,在这种情况下即使失去了其中的一些东西也真的无所谓。但是,对于任何业务关键操作来说,这不是一个好的解决方案。在这些情况下,您必须采用更复杂的架构,具有持久保存操作的方法(例如Azure队列,MSMQ)和一个单独的后台进程(例如Azure工作角色,Win32服务)来处理它们。


8
我想你可能误解了。我确实关心它是否会抛出异常并且失败,但我不想在返回我的数据之前等待该方法完成。另外请看一下我编辑的内容,了解我所处的上下文是否有任何区别。 - George Powell
8
在没有活动请求的ASP.NET上下文中运行代码非常危险。我有一篇博客文章可能可以帮助你,但是如果我不了解你的问题更多信息,我无法确定是否推荐使用该方法。 - Stephen Cleary
2
@StephenCleary 我有类似的需求。在我的例子中,我需要一个批处理引擎在云中运行,我会“ping”端点来启动批处理,但我想立即返回。由于ping它可以启动它,因此它可以处理从那里开始的一切。如果有抛出的异常,那么它们只会记录在我的“BatchProcessLog/Error”表中... - ganders
16
在C#7中,你可以将var _ = MyAsyncMethod();替换为_ = MyAsyncMethod();。这仍然避免了警告CS4014,但它更加明确地表明你不使用该变量。 - Brian
6
我认为OP的意思是,他不希望客户端(HTTP请求)等待将某些东西记录到数据库是否成功。如果记录失败,应用程序仍应完全控制处理该异常,但我们不需要让客户端等待处理该异常。我认为这意味着需要在后台线程上完成工作。是的...为执行异步任务而保留线程很糟糕,但为了处理潜在的异常,必须这样做。 - The Muffin Man

63

Peter Ritchie的答案正是我想要的,而Stephen Cleary关于在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) 中处理,而不是在包围 GetStringData()try/catch 中处理。


11
移除Console.ReadLine();并在MyAsyncMethod中增加一点睡眠/延迟,就不会再看到异常了。 - tymtam

28
我得出了这个解决方案:

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

24

这称为“发射并忘记”,有一个扩展可以实现此功能。

消费任务而不对其进行任何操作,适用于在异步方法中调用异步方法时使用的“发射并忘记”调用。

安装NuGet包

使用:

MyAsyncMethod().Forget();

编辑:最近我一直在使用的另一种方式是:另一种方法

_ = MyAsyncMethod();

23
它并不会做任何事情,只是一个用来消除警告的诡计。请参见 https://github.com/microsoft/vs-threading/blob/master/src/Microsoft.VisualStudio.Threading/TplExtensions.cs#L256 - Paolo Fulgoni
更正:它会执行一系列操作以避免对所做的调用作出反应。 - Mauricio Gracia Gutierrez

13

这并不是最佳实践,你应该尽量避免这样做。

然而,如果你想要“在C#中调用异步方法但不使用await”,你可以在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";
}

6
MyAsyncMethod().Result 不是做同样的事情吗? - maraaaaaaaa

10
我来晚了,但是有一个令人惊叹的库我一直在使用,我没有在其他答案中看到提到它: 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解决方案非常有用。

这对它的信心没有增加吗?https://github.com/brminnick/AsyncAwaitBestPractices/issues/65 - Maja Stamenic
1
那篇文章的作者故意使用了Thread.Sleep而不是await Task.Delay来设计他的BadTask,这是有意为之的。(我不确定为什么)。在几乎所有正常情况下,你不会这样做。对于大多数用例,这个功能都能够正常工作。 - Adam Diament
@AdamDiament他们正在使用Thread.Sleep来模拟在await之前执行大量同步工作的任务(例如,如果您必须在将结果发送到API之前进行计算密集型计算),他们指出,如果任务的初始同步部分需要很长时间或引发异常,那么调用者也会遇到相同的问题,因此这不是一个真正的“安全”fire & forget。 - Harry

1

我猜问题出现了,为什么需要这样做?C# 5.0 中 async 的原因是为了让你可以等待结果。这个方法实际上并不是异步的,而只是在某个时间点被调用,以免过多干扰当前线程。

也许最好启动一个线程并让它自行完成。


2
async 不仅仅是“等待”一个结果。 "await" 暗示着在异步执行 "await" 的同一线程上执行后续代码行。当然,这也可以在没有 "await" 的情况下完成,但会导致大量委托的出现,丧失代码的顺序外观和感受(以及使用 usingtry/catch 功能的能力)。 - Peter Ritchie
3
@PeterRitchie 你可以拥有一个功能上是异步的方法,不使用 await 关键字,也不使用 async 关键字,但是在该方法的定义中没有使用 await 的情况下,使用 async 关键字毫无意义。 - Servy
2
@PeterRitchie 从这个语句中可以看出:"async不仅仅是等待结果。" async关键字(你用反引号括起来暗示了它是一个关键字)的意思只是等待结果,没有其他含义。异步性,作为计算机科学的一般概念,才是超越等待结果的更多含义。 - Servy
1
@Servy async 创建了一个状态机,用于管理异步方法中的任何等待。如果方法中没有await,它仍然会创建该状态机--但该方法不是异步的。如果async方法返回void,则没有需要等待的内容。因此,它不仅仅是等待结果。 - Peter Ritchie
1
只要方法返回一个任务,你就可以等待它。不需要状态机或async关键字。你需要做的(在这种特殊情况下真正发生的事情)是同步运行该方法,然后将其包装在已完成的任务中。我想从技术上讲,你不仅仅是移除状态机;你还需要移除状态机,然后调用Task.FromResult。我假设你(以及编译器的编写者)可以自己添加附言。 - Servy
显示剩余3条评论

1
在具有消息循环的技术中(不确定ASP是否属于其中之一),您可以阻止循环并处理消息,直到任务完成,并使用ContinueWith解除代码的阻塞。
public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

这种方法类似于在ShowDialog上进行阻塞,同时仍然保持UI的响应性。

1
通常异步方法会返回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) - 我已更新函数名称以遵循此建议。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接