作为通过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);
}
}
对我来说,这段代码看起来很简单。这可能看起来有点人为,但这只是因为我尽可能地尝试简化问题。看起来像这样链接的两个ContinueWiths会导致死锁 - 如果我注释掉第一个ContinueWith(它实际上并没有做任何事情),它会正常工作。我也可以通过不给出特定的调度程序来“修复”它(但这对我们来说不是一个可行的解决方案,因为我们的实际代码需要在正确/原始的线程上)。在这里,我将两个ContinueWiths放在一起,但在我们的实际应用程序中,有很多逻辑正在发生,而ContinueWiths最终来自不同的方法。
我知道我可以使用async / await重写这个特定的例子,它会简化一些事情并且似乎可以修复死锁。但是,我们在过去几年中已经编写了大量遗留代码 - 其中大部分都是在异步/等待出现之前编写的,所以它大量使用了ContinueWith。如果我们能够避免它,重写所有逻辑并不是我们现在想要做的事情。像这样的代码在我们遇到的所有其他场景(桌面应用程序,Silverlight应用程序,命令行应用程序等)中运行良好 - 它只是Web API,它给我们带来了这些问题。
是否可以通过一般完成任何可以解决这种僵局的事情?我正在寻找一种解决方案,希望不要重写所有的ContinueWith来使用async / await。
更新
上面的代码是我控制器中的整个代码。我试图用最少量的代码使这个可重复。我甚至在一个全新的解决方案中做到了这一点。我做的全部步骤:
web.config不受模板创建的影响。具体来说,它有这个:
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
</system.web>
答案 0 :(得分:3)
尝试以下操作(未经测试)。它基于AspNetSynchronizationContext.Send
同步执行回调的想法,因此不应导致the same deadlock。这样,我们在随机池线程中输入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()
,对现有代码库进行非常小的更改。
答案 1 :(得分:1)
根据Noseratio的answer,我提出了以下'Safe'版本的ContinueWith。当我更新我的代码以使用这些安全版本时,我不再有死锁。用这些SafeContinueWiths替换我现有的所有ContinueWith可能不会太糟糕......它似乎比重写它们使用async / await更容易和更安全。当这在非ASP.NET内容(WPF应用程序,单元测试等)下执行时,它将回退到标准的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";
}
/// <summary>
/// A version of ContinueWith that does some extra gynastics when running under the ASP.NET Synchronization
/// Context in order to avoid deadlocks. The <see cref="continuationFunction"/> will always be run on the
/// current SynchronizationContext so:
/// Before: task.ContinueWith(t => { ... }, TaskScheduler.FromCurrentSynchronizationContext());
/// After: task.SafeContinueWith(t => { ... });
/// </summary>
public static Task<T> SafeContinueWith<T>(this Task task, Func<Task,T> 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<Task> continuation)
{
var context = SynchronizationContext.Current;
if (!context.IsAspNetContext())
{
return task.ContinueWith(continuation, TaskScheduler.FromCurrentSynchronizationContext());
}
return task.ContinueWith(t => context.Send(_ => continuation(t), null));
}
}
答案 2 :(得分:0)
您可以设置TaskContinuationOptions.ExecuteSynchronously
:
return Task.FromResult(1)
.ContinueWith(_ => { }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, context)
.ContinueWith(_ => Ok(DateTime.Now.ToLongTimeString()), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, context);
还有一个&#34;全球&#34;让它发挥作用的方法;在您的web.config中,将其添加到您的appSettings
:
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
但是,我无法真正推荐全球方法。通过该appsetting,您无法在您的应用中使用async
/ await
。