ASP.NET 4.6异步控制器方法等待

时间:2017-04-24 10:29:07

标签: c# asp.net async-await

我有一个针对.NET 4.6的ASP.NET应用程序,我疯狂地试图找出为什么HttpContext.Current在我的异步MVC控制器操作中第一次等待之后变为空。

我已经检查并三重检查我的项目是针对v4.6,而web.config的targetFramework属性也是4.6。

在等待之前和之后分配

SynchronizationContext.Current,它是正确的,即AspNetSynchronizationContext,而不是遗留的。{/ p>

FWIW,await有问题确实在继续时切换线程,这可能是因为它调用外部I / O绑定代码(异步数据库调用),但这应该不是问题,AFAIU。 / p>

然而,它是! HttpContext.Current变为null的事实导致我的代码出现了许多问题,对我来说没有任何意义。

我已经检查了the usual recommendations,我很肯定我正在做我应该做的一切。我的代码中也绝对没有ConfigureAwait

我所拥有的是HttpApplication实例上的几个异步事件处理程序:

public MvcApplication()
{
    var helper = new EventHandlerTaskAsyncHelper(Application_PreRequestHandlerExecuteAsync);
    AddOnPreRequestHandlerExecuteAsync(helper.BeginEventHandler, helper.EndEventHandler);

    helper = new EventHandlerTaskAsyncHelper(Application_PostRequestHandlerExecuteAsync);
    AddOnPostRequestHandlerExecuteAsync(helper.BeginEventHandler, helper.EndEventHandler);
}

我需要这两个因为自定义授权&清理逻辑,需要异步。 AFAIU,这是支持的,不应该是一个问题。

还有什么可能是我看到这种令人费解的行为的原因?

更新:补充观察。

在等待之前,SynchronizationContext引用保持不变。但它的内部变化介于两者之间,可以在下面的截图中看到!

在AWAIT之前: Before entering await

AFA AWAIT: After continuing

我不确定这与我的问题有什么关系(或者甚至是否)。希望别人能看到它!

1 个答案:

答案 0 :(得分:1)

我决定在HttpContext.Current上定义一只手表并开始踩到#34;等待看到它究竟在哪里变化。毫不奇怪,在我继续操作时,线程被多次切换,这对我来说很有意义,因为路上有多个真正的异步调用。它们都保留了HttpContext.Current实例,因为它们应该如此。

然后我击中违规行......

var observer = new EventObserver();
using (EventMonitor.Instance.Observe(observer, ...))
{
    await plan.ExecuteAsync(...);
}

var events = await observer.Task; // Doh!

简短说明是plan.ExecuteAsync执行许多步骤,这些步骤通过专用线程以非阻塞方式报告给专门的事件日志。这是商业软件,报告事件的模式在整个代码中得到了广泛的使用。大多数情况下,这些事件与呼叫者无直接关系。但是一个或两个地方的特殊之处在于调用者想知道由于执行某个代码而发生了哪些事件。使用EventObserver实例时的情况,如上所示。

为了等待处理和观察所有相关事件,await observer.Task是必需的。有问题的任务来自观察员拥有的TaskCompletionSource实例。一旦所有事件都进入,所以从处理事件的线程调用源SetResult。我最初对这个细节的实现非常天真 - 如下:

public class EventObserver : IObserver<T>
{
    private readonly ObservedEvents _events = new ObservedEvents();

    private readonly TaskCompletionSource<T> _source;

    private readonly SynchronizationContext _capturedContext;

    public EventObserver()
    {
        _source = new TaskCompletionSource<T>();

        // Capture the current synchronization context.
        _capturedContext = SynchronizationContext.Current;
    }

    void OnCompleted()
    {
        // Apply the captured synchronization context.
        SynchronizationContext.SetSynchronizationContext(_capturedContext);
        _source.SetResult(...);
    }
}

我现在可以看到SetSynchronizationContext之前调用SetResult并不是在做我希望的事情。目标是将原始同步上下文应用于行await observer.Task的延续。

现在的问题是:我该如何正确地做到这一点?我猜它会在某个地方进行明确的ContinueWith通话。

<强>更新

这就是我的所作所为。我传递了TaskCreationOptions.RunContinuationsAsynchronously选项TaskCompletionSource ctor并修改了我的EventObserver类上的Task属性以包含显式同步的continuation:

public Task<T> Task
{
    get
    {
        return _source.Task.ContinueWith(t =>
        {
            if (_capturedContext != null)
            {
                SynchronizationContext.SetSynchronizationContext(_capturedContext);
            }

            return t.Result;
        });
    }
}

现在,当代码调用await observer.Task时,延续将确保首先输入正确的上下文。到目前为止,它似乎正常工作!