用的WebAPI ContinueWiths死锁 [英] Deadlock with ContinueWiths in WebAPI

查看:258
本文介绍了用的WebAPI ContinueWiths死锁的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我们已经运行到了很多死锁,因为揭露一些现有的code以上的Web API的一部分。我已经能够提炼出的问题到这个非常简单的例子,将永远挂:

 公共类myController的:ApiController
{
    公共任务的get()
    {
        VAR上下文= TaskScheduler.FromCurrentSynchronizationContext();        返回Task.FromResult(1)
            .ContinueWith(_ => {},上下文)
            .ContinueWith(_ =>确定(DateTime.Now.ToLongTimeString()),背景);
    }
}

对于我来说,code看起来很简单。这似乎有点做作,但是那只是因为我已经尝试简化问题,尽可能多地。这似乎有捆绑,像这样会导致僵局2 ContinueWiths - 如果我注释掉第一ContinueWith(即是不是真的做任何事情反正),它会工作得很好。我也可以修复它不给具体的调度程序(但这并不是因为我们真正的code对我们是一个可行的解决方案需要正确的/原始的线程上)。在这里,我已经把两个ContinueWiths旁边对方,但在我们的实际应用中有很多正在发生的逻辑和ContinueWiths最终从不同的方法来。

我知道我可以重新编写使用异步这个特殊的例子/等待,这将简化事情,似乎解决僵局。但是,我们有一吨已经写在过去几年遗留code的 - 而且大部分是写之前,异步/的await出来所以它使用ContinueWith的巨资。重新编写所有的逻辑是不是我们想要做的,现在如果我们能够避免它。 code这样的,我们碰到的(桌面应用程序,Silverlight应用程序,命令行应用程序等)的所有其他情况一直很好 - 它只是网页API是给我们这些问题。

有没有什么可以做的一般的,可以解决这种僵局?我在寻找,将有望不涉及重新编写所有ContinueWith的使用解决方案异步/游览车。

更新:

上面的code是在我的控制器在整个code。我试图使这一可重复使用code的最小量。我甚至在一个全新的解决方案做到了这一点。完整的步骤,我所做的:


    在Windows 7上
  1. 从Visual Studio的2013更新1(与.NET框架4.5.1),创建一个使用ASP.NET Web应用程序模板创建新项目

  2. 选择网页API作为模板(在下一屏)

  3. 在我原来的code给出的例子替换自动创建的Values​​Controller的get()方法

  4. 按F5键启动应用程序并导航到./api/values​​ - 请求将永远挂

  5. 我试过托管在IIS中的网站,以及(而不是使用IIS防爆preSS)

  6. 我也尝试更新所有不同的NuGet软件包,我是在最新的一切

在web.config是从模板创建的内容不变。具体地,它具有这样的:

 <&的System.Web GT;
   <编译调试=真targetFramework =4.5/>
   <的httpRuntime targetFramework =4.5/>
< /system.web>


解决方案

根据Noseratio的回答,我想出了下面的'安全'版本ContinueWith的。当更新我的code使用这些安全的版本,我没有了死锁。更换这些SafeContinueWiths现有的所有ContinueWiths可能不会太糟糕了....它肯定似乎比重新编写他们使用异步/的await更加容易和安全。而当这个执行下non-ASP.NET上下文(WPF应用程序,单元测试等),这将让我的的有完美的向后兼容回落到标准ContinueWith行为。

我仍然不知道这是最好的解决方案。看起来这是一个pretty严厉的方法,是必要的code,似乎如此简单。

随着中说,我是presenting这个答案的情况下,它从别人触发一个伟大的想法。我觉得这不可能是理想的解决方案。

新的控制器code:

 公共任务获得()
{
    返回Task.FromResult(1)
               .SafeContinueWith(_ => {})
               .SafeContinueWith(_ =>确定(DateTime.Now.ToLongTimeString()));
}

和则实际执行SafeContinueWith的:

 公共静态类TaskExtensions
{
    私人静态布尔IsAspNetContext(这方面的SynchronizationContext)
    {
        //也许不是最好的方式来检测AspNetSynchronizationContext,但它的工作原理现在
        返回上下文= NULL&放大器;!&安培; context.GetType()==全名System.Web.AspNetSynchronizationContext。
    }    ///<总结>
    ///,做一些额外的gynastics ContinueWith的版本时,下ASP.NET运行同步
    ///语境,以避免死锁。在<见CREF =continuationFunction/>总是会在运行
    ///当前的SynchronizationContext这样:
    ///之前:task.ContinueWith(叔= GT; {...},TaskScheduler.FromCurrentSynchronizationContext());
    ///后:task.SafeContinueWith(T => {...});
    ///< /总结>
    公共静态任务< T> SafeContinueWith< T>(这个任务任务,Func键<任务,T> continuationFunction)
    {
        //取得上下文
        VAR语境= SynchronizationContext.Current;        //如果我们不是在ASP.NET的世界里,我们可以推迟到标准ContinueWith
        如果(!context.IsAspNetContext())
        {
            返回task.ContinueWith(continuationFunction,TaskScheduler.FromCurrentSynchronizationContext());
        }        //否则,我们需要我们继续进行在后台线程上运行,然后同步评估
        //在拍摄方面延续功能arive的结果值
        返回task.ContinueWith(T =>
        {
            VAR的结果=默认(T);
            context.Send(_ =>结果= continuationFunction(T),NULL);
            // TODO:验证发送真的完全同步?我认为这是通过合同要求?
            //但是我不知道我愿意相信,如果我结束了在producion code。使用本。
            返回结果;
        });
    }    //同上,但对于非通用任务输入,所以简单一点
    公共静态任务SafeContinueWith(该任务的任务,行动<任务>续)
    {
        VAR语境= SynchronizationContext.Current;
        如果(!context.IsAspNetContext())
        {
            返回task.ContinueWith(延续,TaskScheduler.FromCurrentSynchronizationContext());
        }        返回task.ContinueWith(T => context.Send(_ =>延续(T),NULL));
    }
}

We've been running into a lot of deadlocks as part of exposing some of existing code over Web API. I've been able to distill the problem down to this very simple example that will hang forever:

public class MyController : ApiController
{
    public Task Get()
    {
        var context = TaskScheduler.FromCurrentSynchronizationContext();

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

To me, that code seems simple enough. This might seem a bit contrived but that's only because I've tried simplifying the problem as much as possible. It seems having two ContinueWiths chained like this will cause the deadlock - if I comment out the first ContinueWith (that isn't really doing anything anyway), it will work just fine. I can also 'fix' it by not giving the specific scheduler (but that isn't a workable solution for us since our real code needs to be on the correct/original thread). Here, I've put the two ContinueWiths right next to each other but in our real application there is a lot of logic that is happening and the ContinueWiths end up coming from different methods.

I know I could re-write this particular example using async/await and it would simplify things and seems to fix the deadlock. However, we have a ton of legacy code that has been written over the past few years - and most of it was written before async/await came out so it uses ContinueWith's heavily. Re-writing all that logic isn't something we'd like to do right now if we can avoid it. Code like this has worked fine in all the other scenarios we've run into (Desktop App, Silverlight App, Command Line App, etc.) - it's just Web API that is giving us these problems.

Is there anything that can be done generically that can solve this kind of deadlock? I'm looking for a solution that would hopefully not involve re-writing all ContinueWith's to use async/await.

Update:

The code above is the entire code in my controller. I've tried to make this reproducible with the minimal amount of code. I've even done this in a brand new solution. The full steps that I did:

  1. From Visual Studio 2013 Update 1 on Windows 7 (with .NET Framework 4.5.1), create a new Project using the ASP.NET Web Application Template
  2. Select Web API as the template (on the next screen)
  3. Replace the Get() method in the auto-created ValuesController with the example given in my original code
  4. Hit F5 to start the app and navigate to ./api/values - the request will hang forever
  5. I've tried hosting the web site in IIS as well (instead of using IIS Express)
  6. I also tried updating all the various Nuget packages so I was on the latest of everything

The web.config is untouched from what the template created. Specifically, it has this:

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

解决方案

Based on Noseratio's answer, I came up with the following 'Safe' versions of ContinueWith. When I update my code to use these safe versions, I don't have anymore deadlocks. Replacing all my existing ContinueWiths with these SafeContinueWiths probably won't be too bad....it certainly seems easier and safer than re-writing them to use async/await. And when this executes under non-ASP.NET contexts (WPF App, Unit Tests, etc.), it will fall back to the standard ContinueWith behavior so I should have perfect backwards compatability.

I'm still not sure this is the best solution. It seems like this is a pretty heavy-handed approach that is necessary for code that seems so simple.

With that said, I'm presenting this answer in case it triggers a great idea from somebody else. I feel like this can't be the ideal solution.

New controller code:

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

And then the actual implementation of 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));
    }
}

这篇关于用的WebAPI ContinueWiths死锁的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆