使用SynchronizationContext时async / await死锁

时间:2015-12-08 08:30:11

标签: c# .net asynchronous

根据this link

  

当您在等待具有await关键字的方法时,编译器会代表您生成大量代码。其中一个目的   action是处理与UI线程的同步。钥匙
  此功能的组件是SynchronizationContext.Current   获取当前线程的同步上下文   SynchronizationContext.Current的填充取决于具体情况   你所处的环境。任务的GetAwaiter方法查找
  SynchronizationContext.Current。如果当前同步上下文   不是null,传递给那个awaiter的延续会   回发到同步上下文。

     

当使用阻塞方式使用新异步语言功能的方法时,如果发生死锁,最终会出现死锁   您有SynchronizationContext   以阻塞的方式使用这些方法(等待任务   使用Wait方法或直接从Result中获取结果   任务的属性),你将同时阻止主线程   时间。当最终任务在该方法内部完成时   线程池,它将调用延续回发   因为SynchronizationContext.Current是主线​​程   可用和捕获。但这里有一个问题:UI线程   被阻止,你有一个僵局!

    public class HomeController : Controller
    {    
        public ViewResult CarsSync() 
        {
            SampleAPIClient client = new SampleAPIClient();
            var cars = client.GetCarsInAWrongWayAsync().Result;
            return View("Index", model: cars);
        }
    }

    public class SampleAPIClient 
    {
        private const string ApiUri = "http://localhost:17257/api/cars";
        public async Task<IEnumerable<Car>> GetCarsInAWrongWayAsync()
        {
            using (var client = new HttpClient()) 
            {
                var response = await client.GetAsync(ApiUri);

                // Not the best way to handle it but will do the work for demo purposes
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsAsync<IEnumerable<Car>>();
            }
        }
    }

我无法理解上面语句的粗体部分,但是当我测试上面的代码时,它会按预期死锁。 但我仍然无法理解为什么UI线程被阻止了?

在这种情况下,可用的SynchronizationContext是什么?它是UI线程吗?

2 个答案:

答案 0 :(得分:11)

我完全解释了这一点in my own blog post,但在此重申......

默认情况下,

await将捕获当前的“上下文”并在该上下文中恢复其async方法。除非是SynchronizationContext.Current,否则此上下文为null,在此情况下为TaskScheduler.Current

当您有一个单线程SynchronizationContext并且阻止代表异步代码的任务时(例如,使用Task.WaitTask<T>.Result)。请注意,阻塞导致死锁,而不仅仅是SynchronizationContext;适当的解决方案(几乎总是)是使调用代码异步(例如,将Task.Wait / Task<T>.Result替换为await)。在ASP.NET上尤其如此。

  

但是我仍然无法理解为什么UI线程被阻止了?

您的示例正在ASP.NET上运行;没有UI线程。

  

可用的SynchronizationContext是什么?

当前SynchronizationContext应该是AspNetSynchronizationContext的实例,这是一个表示ASP.NET请求的上下文。此上下文一次只允许一个线程。

所以,走过你的例子:

当请求进入此操作时,CarsSync将在该请求上下文中开始执行。它继续这一行:

var cars = client.GetCarsInAWrongWayAsync().Result;

与此基本相同:

Task<IEnumerable<Car>> carsTask = client.GetCarsInAWrongWayAsync();
var cars = carsTask.Result;

因此,它会调用GetCarsInAWrongWayAsync,直到它遇到第一个awaitGetAsync调用)。此时,GetCarsInAWrongWayAsync捕获其当前上下文(ASP.NET请求上下文)并返回不完整的Task<IEnumerable<Car>>GetAsync下载完成后,GetCarsInAWrongWayAsync将继续在该ASP.NET请求上下文上执行,并(最终)完成已返回的任务。

但是,只要GetCarsInAWrongWayAsync返回未完成的任务,CarsSync就会阻止当前线程,等待该任务完成。请注意,当前线程位于该ASP.NET请求上下文中,因此CarsSync将阻止GetCarsInAWrongWayAsync恢复执行,从而导致死锁。

最后一点,GetCarsInAWrongWayAsync是一种OK方法。如果它使用ConfigureAwait(false)会更好,但它实际上并不是错误的CarsSync是导致死锁的方法;它对Task<T>.Result 的调用是错误的。适当的解决方法是更改​​CarsSync

public class HomeController : Controller
{    
  public async Task<ViewResult> CarsSync() 
  {
    SampleAPIClient client = new SampleAPIClient();
    var cars = await client.GetCarsInAWrongWayAsync();
    return View("Index", model: cars);
  }
}

答案 1 :(得分:1)

关键是一些SynchronizationContext只允许单个线程同时运行代码。一个帖子正在调用ResultWait。当异步方法想要输入时,它就可以了。

某些SynchronizationContext是多线程的,并且不会出现问题。