Ste*_*iel 5 c# deadlock task-parallel-library asp.net-web-api
作为通过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);
}
}
Run Code Online (Sandbox Code Playgroud)
对我来说,这段代码看起来很简单.这可能看起来有点人为,但这只是因为我尽可能地尝试简化问题.看起来像这样链接的两个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>
Run Code Online (Sandbox Code Playgroud)
根据 Noseratio 的回答,我想出了以下“安全”版本的ContinueWith。当我更新代码以使用这些安全版本时,我不再遇到死锁。用这些 SafeContinueWith 替换我现有的所有ContinueWith可能不会太糟糕......它肯定比重写它们以使用 async/await 更容易、更安全。当它在非 ASP.NET 上下文(WPF 应用程序、单元测试等)下执行时,它将回退到标准的ContinueWith 行为,因此我应该具有完美的向后兼容性。
我仍然不确定这是最好的解决方案。看起来这是一种相当严厉的方法,对于看起来如此简单的代码来说是必要的。
话虽如此,我提出这个答案是为了防止它引发其他人的好主意。我觉得这不是理想的解决方案。
新控制器代码:
public Task Get()
{
return Task.FromResult(1)
.SafeContinueWith(_ => { })
.SafeContinueWith(_ => Ok(DateTime.Now.ToLongTimeString()));
}
Run Code Online (Sandbox Code Playgroud)
然后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));
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1093 次 |
| 最近记录: |