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


321

我有一个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

1
如果您不想等待结果,唯一的选择就是忽略/抑制警告。如果您确实想等待结果/例外,那么MyAsyncMethod().Wait()
彼得·里奇

1
关于您的修改:对我来说这没有意义。假设请求后1秒钟将响应发送到客户端,然后2秒钟后您的异步方法将引发异常。您将如何处理该异常?如果您的回复已经发送,则无法将其发送给客户端。您还会用它做什么?

2
@Romoku足够公平。无论如何,假设有人在看日志。:)

2
ASP.NET Web API方案的一种变体是一个长期存在的过程(例如Windows服务)中的自托管 Web API,在该过程中,请求创建了一个冗长的后台任务来做一些昂贵的事情,但仍然希望使用HTTP 202(已接受)快速获得响应。
大卫·鲁宾

Answers:


186

如果要“异步”获取异常,则可以执行以下操作:

  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)意思。


7
我把它变成了一个扩展方法Task:用法:MyAsyncMethod()。PerformAsyncTaskWithoutAwait(t => log.ErrorFormat(“调用MyAsyncMethod时发生错误:\ n {0}”,t.Exception));
Mark Avenius

2
下注者。注释?如果答案有问题,我想知道和/或解决。
彼得·里奇

2
嘿-我没有投票,但是...您能解释一下您的更新吗?不应等待呼叫,因此,如果您那样做,那么您就等待呼叫,只是不要在捕获的上下文中继续...
Bartosz

1
在这种情况下,“等待”不是正确的描述。ConfiguratAwait(false)直到任务完成,该行之后的行才执行,但是当前线程不会为此“等待”(即阻塞),因此下一行与await调用异步调用。否则ConfigureAwait(false),下一行将在原始Web请求上下文中执行。使用ConfigurateAwait(false)它在与异步方法(任务)相同的上下文中执行,释放原始上下文/线程以继续执行...
Peter Ritchie

15
ContinueWith版本与try {await} catch {}版本不同。在第一个版本中,ContinueWith()之后的所有内容将立即执行。最初的任务被解雇并被遗忘。在第二个版本中,catch {}之后的所有内容仅在初始任务完成后才执行。第二个版本等效于“ await MyAsyncMethod()。ContinueWith(t => Console.WriteLine(t.Exception),TaskContinuationOptions.OnlyOnFaulted).ConfigureAwait(fals);
Thanasis Ioannidis

67

您应该首先考虑制作GetStringData一个async方法,并将其await从中返回MyAsyncMethod

如果您完全确定不需要处理异常MyAsyncMethod 何时完成,则可以执行以下操作:

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

顺便说一句,这不是一个“常见问题”。这是非常罕见的要执行一些代码,并不在乎它是否完成,并不在乎是否成功完成。

更新:

由于您使用的是ASP.NET并希望早日返回,因此您可能会发现关于此主题的博客文章很有用。但是,ASP.NET并非为此设计的,因此不能保证您的代码将在返回响应后运行。ASP.NET将尽其所能使其运行,但不能保证。

所以,这是一些简单的罚款解决方案一样折腾事件写入日志在它不真的,如果你失去了几这里有关系。对于任何类型的关键业务操作来说,这都不是一个好的解决方案。在这种情况下,您必须采用更复杂的体系结构,并采用一种持久的方式来保存操作(例如,Azure队列,MSMQ),并使用单独的后台进程(例如,Azure Worker角色,Win32服务)来处理它们。


2
我想您可能误会了。我在意它是否会引发异常并失败,但是我不想在返回数据之前先等待该方法。如果这有什么区别,还请参见我对所使用上下文的编辑。
乔治·鲍威尔,

5
@GeorgePowell:在ASP.NET上下文中没有活动请求运行代码是非常危险的。我有一篇博客文章可能会为您提供帮助,但是在不了解您的更多问题的情况下,我无法说出是否推荐这种方法。
Stephen Cleary

@StephenCleary我也有类似的需求。在我的示例中,我/需要批处理引擎在云中运行,我将“ ping”端点以开始批处理,但我想立即返回。自从ping它开始以来,它就可以处理那里的所有内容。如果有异常抛出,那么它们只会被记录在我的“ BatchProcessLog / Error”表中...
ganders

8
在C#7中,您可以替换var _ = MyAsyncMethod();_ = MyAsyncMethod();。这仍然可以避免发出CS4014警告,但可以使您更清楚地知道您没有使用该变量。
布赖恩

48

Peter Ritchie的答案是我想要的,Stephen Cleary的有关在ASP.NET中尽早返回的文章非常有帮助。

但是,作为一个更普遍的问题(并非特定于ASP.NET上下文),以下控制台应用程序演示了Peter的回答用法和用法: 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)try/ catch左右GetStringData()


9
删除Console.ReadLine();并添加一点睡眠/延迟MyAsyncMethod,您将永远不会看到异常。
tymtam '16

17

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

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


2

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

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


2
async不仅仅是“等待”结果。“ await”表示“ await”之后的行在调用“ await”的同一线程上异步执行。当然,无需“等待”就可以完成此操作,但是最终您会得到很多委托,并且失去了代码的顺序外观(以及使用usingtry/catch... 的能力
Peter Ritchie

1
@PeterRitchie您可以使用在功能上异步,不使用await关键字且不使用async关键字的方法,但是在async不使用await该方法的定义的情况下使用关键字毫无意义。
Servy 2013年

1
@PeterRitchie在声明中说:“ async不仅仅是“等待”结果。” 在async(你暗示它在反引号括起来的关键字)关键字意味着没有超过等待结果。与一般的CS概念一样,它是异步的,不仅仅意味着等待结果。
2013年

1
@Servy async创建一个状态机,该状态机管理async方法中的所有等待。如果await方法中没有,它仍会创建该状态机-但该方法不是异步的。如果该async方法返回void,则没有任何等待。因此,它不仅仅是等待结果。
彼得·里奇

1
@PeterRitchie只要方法返回任务,您就可以等待它。不需要状态机或async关键字。您需要做的所有事情(以及在这种特殊情况下最终在状态机中真正发生的所有事情)是该方法同步运行,然后包装在已完成的任务中。从技术上讲,我认为您不只是删除状态机,而是删除状态机。您删除状态机,然后调用Task.FromResult。我以为您(还有编译器作者)可以自己添加附录。
Servy

0

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

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

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


0

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

https://github.com/brminnick/AsyncAwaitBestPractices

如果需要“解雇”,请在任务上调用扩展方法。

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

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

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

它还提供了可实现ACommand的AsyncCommands,这对我的MVVM Xamarin解决方案非常有用


-1

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

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

-1

如果您真的想这样做。只需解决“不用等待就在C#中调用异步方法”,您就可以在中执行异步方法Task.Run。此方法将等待MyAsyncMethod完成。

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

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


-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)-我已更新函数名称以遵循此建议。

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.