在Web API中使用ContinueWith进行死锁

时间:2014-05-13 18:26:52

标签: c# asp.net-web-api task-parallel-library deadlock

作为通过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。

更新

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

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

    <system.web>
       <compilation debug="true" targetFramework="4.5" />
       <httpRuntime targetFramework="4.5" />
    </system.web>
    

3 个答案:

答案 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