同步等待异步操作,以及为什么Wait()在这里冻结程序

Seb*_*zus 301 .net c# task-parallel-library async-await windows-store-apps

前言:我正在寻找解释,而不仅仅是解决方案.我已经知道了解决方案.

尽管花了几天时间研究MSDN关于基于任务的异步模式(TAP),异步和等待的文章,但我仍然对一些更精细的细节感到困惑.

我正在为Windows Store应用程序编写一个记录器,我想支持异步和同步日志记录.异步方法遵循TAP,同步方法应该隐藏所有这些,并且看起来像普通方法一样工作.

这是异步日志记录的核心方法:

private async Task WriteToLogAsync(string text)
{
    StorageFolder folder = ApplicationData.Current.LocalFolder;
    StorageFile file = await folder.CreateFileAsync("log.log",
        CreationCollisionOption.OpenIfExists);
    await FileIO.AppendTextAsync(file, text,
        Windows.Storage.Streams.UnicodeEncoding.Utf8);
}
Run Code Online (Sandbox Code Playgroud)

现在相应的同步方法......

版本1:

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Wait();
}
Run Code Online (Sandbox Code Playgroud)

这看起来是正确的,但它不起作用.整个程序永远冻结.

版本2:

嗯..也许任务没有开始?

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.Start();
    task.Wait();
}
Run Code Online (Sandbox Code Playgroud)

这引发了 InvalidOperationException: Start may not be called on a promise-style task.

版本3:

嗯.. Task.RunSynchronously听起来很有希望.

private void WriteToLog(string text)
{
    Task task = WriteToLogAsync(text);
    task.RunSynchronously();
}
Run Code Online (Sandbox Code Playgroud)

这引发了 InvalidOperationException: RunSynchronously may not be called on a task not bound to a delegate, such as the task returned from an asynchronous method.

版本4(解决方案):

private void WriteToLog(string text)
{
    var task = Task.Run(async () => { await WriteToLogAsync(text); });
    task.Wait();
}
Run Code Online (Sandbox Code Playgroud)

这有效.所以,2和3是错误的工具.但1?1有什么不对,4有什么不同?1导致冻结的原因是什么?任务对象有问题吗?是否存在非明显的死锁?

SLa*_*aks 178

await您的异步方法内试图回到UI线程.

由于UI线程忙于等待整个任务完成,因此您遇到了死锁.

移动异步调用以Task.Run()解决问题.
因为异步调用现在在线程池线程上运行,所以它不会尝试返回到UI线程,因此一切正常.

或者,您可以StartAsTask().ConfigureAwait(false)在等待内部操作之前调用它以使其返回到线程池而不是UI线程,从而完全避免死锁.

  • 在这种情况下,`ConfigureAwait(false)`是适当的解决方案.由于它不需要在捕获的上下文中调用回调,因此不应该.作为一种API方法,它应该在内部处理它,而不是强迫所有调用者移出UI上下文. (13认同)
  • +1.这是另外一个解释 - [等待,用户界面和死锁!哦,我的!](http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115163.aspx) (9认同)

Ste*_*ary 47

async从同步代码调用代码可能非常棘手.

在博客上解释了这种僵局全部原因.简而言之,默认情况下会在每个开头保存一个"上下文",await用于恢复该方法.

因此,如果在UI上下文中调用awaitasync方法,则在完成时,该方法会尝试重新输入该上下文以继续执行.不幸的是,使用Wait(或Result)的代码将阻止该上下文中的线程,因此该async方法无法完成.

避免这种情况的指导原则是:

  1. 使用ConfigureAwait(continueOnCapturedContext: false)尽可能多地.这使您的async方法可以继续执行而无需重新输入上下文.
  2. 一直使用async.使用await而不是ResultWait.

如果您的方法是自然异步的,那么您(可能)不应该公开同步包装器.

  • @Zapnologica:从 VS2015 开始,`catch` 块中支持 `await`。如果您使用的是旧版本,[您可以将异常分配给局部变量并在 catch 块之后执行`await`](http://blog.stephencleary.com/2014/06/await-in-catch -和-finally.html)。 (2认同)

pix*_*xel 5

这是我所做的

private void myEvent_Handler(object sender, SomeEvent e)
{
  // I dont know how many times this event will fire
  Task t = new Task(() =>
  {
    if (something == true) 
    {
        DoSomething(e);  
    }
  });
  t.RunSynchronously();
}
Run Code Online (Sandbox Code Playgroud)

工作正常,不阻塞UI线程