在WebAPI中使用ContinueWith进行死锁

作为通过Web API公开一些现有代码的一部分,我们遇到了很多死锁。 我已经能够将这个问题提炼到这个非常简单的例子中,这个例子将永远挂起:

public class MyController : ApiController { public Task Get() { var context = TaskScheduler.FromCurrentSynchronizationContext(); return Task.FromResult(1) .ContinueWith(_ => { }, context) .ContinueWith(_ => Ok(DateTime.Now.ToLongTimeString()), context); } } 

对我来说,这段代码看起来很简单。 这可能看起来有点人为,但这只是因为我尝试尽可能地简化问题。 看起来像这样链接的两个ContinueWith会导致死锁 – 如果我注释掉第一个ContinueWith(实际上并没有做任何事情),它会正常工作。 我也可以通过不给出特定的调度程序来“修复”它(但这对我们来说不是一个可行的解决方案,因为我们的实际代码需要在正确/原始的线程上)。 在这里,我将两个ContinueWiths放在一起,但在我们的实际应用中,有很多逻辑正在发生,而ContinueWiths最终来自不同的方法。

我知道我可以使用async / await重写这个特定的例子,它会简化一些事情,似乎可以修复死锁。 但是,我们在过去几年中已经编写了大量遗留代码 – 其中大部分都是在异步/等待出现之前编写的,所以它大量使用了ContinueWith。 如果我们能够避免它,重写所有逻辑并不是我们现在想做的事情。 像这样的代码在我们遇到的所有其他场景(桌面应用程序,Silverlight应用程序,命令行应用程序等)中运行良好 – 它只是给我们这些问题的Web API。

有没有什么可以一般地解决这种僵局? 我正在寻找一种解决方案,希望不会重新编写所有的ContinueWith来使用async / await。

更新:

上面的代码是我控制器中的整个代码。 我试图用最少量的代码使这个可重复。 我甚至在一个全新的解决方案中做到了这一点。 我做的全部步骤:

  1. 从Windows 7上的Visual Studio 2013 Update 1(使用.NET Framework 4.5.1),使用ASP.NET Web应用程序模板创建一个新项目
  2. 选择Web API作为模板(在下一个屏幕上)
  3. 使用我原始代码中给出的示例替换自动创建的ValuesController中的Get()方法
  4. 按F5启动应用程序并导航到./api/values – 请求将永久挂起
  5. 我也试过在IIS中托管网站(而不是使用IIS Express)
  6. 我也尝试更新所有各种Nuget包,所以我在最新的一切

web.config不受模板创建的影响。 具体来说,它具有:

     

尝试以下(未经测试)。 它基于AspNetSynchronizationContext.Send同步执行回调的想法,因此不应导致相同的死锁 。 这样,我们在随机池线程上输入AspNetSynchronizationContext

 public class MyController : ApiController { public Task Get() { // should be AspNetSynchronizationContext var context = SynchronizationContext.Current; return Task.FromResult(1) .ContinueWith(_ => { }, TaskScheduler.Default) .ContinueWith(_ => { object result = null; context.Send(__ => { result = Ok(DateTime.Now.ToLongTimeString()); }, null); return result; }, TaskScheduler.Default); } } 

根据评论更新 ,显然它可以工作并消除死锁。 此外,我将在此解决方案之上构建一个自定义任务调度程序,并使用它而不是TaskScheduler.FromCurrentSynchronizationContext() ,对现有代码库进行非常小的更改。

根据Noseratio的回答 ,我提出了以下’Safe’版本的ContinueWith。 当我更新我的代码以使用这些安全版本时,我不再有死锁。 用这些SafeContinueWiths替换我现有的所有ContinueWiths可能不会太糟糕……它似乎比重写它们使用async / await更容易和更安全。 当这在非ASP.NET上下文(WPF应用程序,unit testing等)下执行时,它将回退到标准的ContinueWith行为,因此我应该具有完美的向后兼容性。

我仍然不确定这是最好的解决方案。 对于看起来如此简单的代码来说,这似乎是一种非常严厉的方法。

话虽如此,我提出这个答案,以防它引发别人的好主意。 我觉得这不是理想的解决方案。

新控制器代码:

 public Task Get() { return Task.FromResult(1) .SafeContinueWith(_ => { }) .SafeContinueWith(_ => Ok(DateTime.Now.ToLongTimeString())); } 

然后是SafeContinueWith的实际实现:

 public static class TaskExtensions { private static bool IsAspNetContext(this SynchronizationContext context) { //Maybe not the best way to detect the AspNetSynchronizationContext but it works for now return context != null && context.GetType().FullName == "System.Web.AspNetSynchronizationContext"; } ///  /// A version of ContinueWith that does some extra gynastics when running under the ASP.NET Synchronization /// Context in order to avoid deadlocks. The  will always be run on the /// current SynchronizationContext so: /// Before: task.ContinueWith(t => { ... }, TaskScheduler.FromCurrentSynchronizationContext()); /// After: task.SafeContinueWith(t => { ... }); ///  public static Task SafeContinueWith(this Task task, Func continuationFunction) { //Grab the context var context = SynchronizationContext.Current; //If we aren't in the ASP.NET world, we can defer to the standard ContinueWith if (!context.IsAspNetContext()) { return task.ContinueWith(continuationFunction, TaskScheduler.FromCurrentSynchronizationContext()); } //Otherwise, we need our continuation to be run on a background thread and then synchronously evaluate // the continuation function in the captured context to arive at the resulting value return task.ContinueWith(t => { var result = default(T); context.Send(_ => result = continuationFunction(t), null); //TODO: Verify that Send really did complete synchronously? I think it's required to by Contract? // But I'm not sure I'd want to trust that if I end up using this in producion code. return result; }); } //Same as above but for non-generic Task input so a bit simpler public static Task SafeContinueWith(this Task task, Action continuation) { var context = SynchronizationContext.Current; if (!context.IsAspNetContext()) { return task.ContinueWith(continuation, TaskScheduler.FromCurrentSynchronizationContext()); } return task.ContinueWith(t => context.Send(_ => continuation(t), null)); } } 

您可以设置TaskContinuationOptions.ExecuteSynchronously

 return Task.FromResult(1) .ContinueWith(_ => { }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, context) .ContinueWith(_ => Ok(DateTime.Now.ToLongTimeString()), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, context); 

还有一种“全球化”的方式来实现这一目标; 在您的web.config中,将其添加到您的appSettings

  

但是,我不能真正推荐全球方法。 使用该appsetting,您不能在应用程序中使用async / await