AspNetSynchronizationContext 并等待 ASP.NET 中的延续 [英] AspNetSynchronizationContext and await continuations in ASP.NET

查看:18
本文介绍了AspNetSynchronizationContext 并等待 ASP.NET 中的延续的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在异步 ASP.NET Web API 控制器方法中的 await 之后,我注意到一个意外的(我会说是冗余的)线程切换.

I noticed an unexpected (and I'd say, a redundant) thread switch after await inside asynchronous ASP.NET Web API controller method.

例如,下面我希望在位置 #2 和 3# 处看到相同的 ManagedThreadId,但大多数情况下我在 #3 处看到不同的线程:

For example, below I'd expect to see the same ManagedThreadId at locations #2 and 3#, but most often I see a different thread at #3:

public class TestController : ApiController
{
    public async Task<string> GetData()
    {
        Debug.WriteLine(new
        {
            where = "1) before await",
            thread = Thread.CurrentThread.ManagedThreadId,
            context = SynchronizationContext.Current
        });

        await Task.Delay(100).ContinueWith(t =>
        {
            Debug.WriteLine(new
            {
                where = "2) inside ContinueWith",
                thread = Thread.CurrentThread.ManagedThreadId,
                context = SynchronizationContext.Current
            });
        }, TaskContinuationOptions.ExecuteSynchronously); //.ConfigureAwait(false);

        Debug.WriteLine(new
        {
            where = "3) after await",
            thread = Thread.CurrentThread.ManagedThreadId,
            context = SynchronizationContext.Current
        });

        return "OK";
    }
}

我查看了 AspNetSynchronizationContext.Post,本质上归结为:

I've looked at the implementation of AspNetSynchronizationContext.Post, essentially it comes down to this:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;

因此,ThreadPool 上安排延续,而不是内联. 这里,ContinueWith 使用 TaskScheduler.Current,根据我的经验,它始终是 ASP.NET 中 ThreadPoolTask​​Scheduler 的一个实例(但不一定是这样,见下文).

Thus, the continuation is scheduled on ThreadPool, rather than gets inlined. Here, ContinueWith uses TaskScheduler.Current, which in my experience is always an instance of ThreadPoolTaskScheduler inside ASP.NET (but it doesn't have to be that, see below).

我可以使用 ConfigureAwait(false) 或自定义等待器消除像这样的冗余线程切换,但这会取消 HTTP 请求状态属性的自动流动,例如 HttpContext.Current.

I could eliminate a redundant thread switch like this with ConfigureAwait(false) or a custom awaiter, but that would take away the automatic flow of the HTTP request's state properties like HttpContext.Current.

AspNetSynchronizationContext.Post 的当前实现还有另一个副作用.在以下情况下会导致死锁:

There's another side effect of the current implementation of AspNetSynchronizationContext.Post. It results in a deadlock in the following case:

await Task.Factory.StartNew(
    async () =>
    {
        return await Task.Factory.StartNew(
            () => Type.Missing,
            CancellationToken.None,
            TaskCreationOptions.None,
            scheduler: TaskScheduler.FromCurrentSynchronizationContext());
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    scheduler: TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();

这个例子,虽然有点做作,但显示了如果 TaskScheduler.CurrentTaskScheduler.FromCurrentSynchronizationContext(),即,由 AspNetSynchronizationContext 制成,可能会发生什么代码>.它不使用任何阻塞代码,并且在 WinForms 或 WPF 中可以顺利执行.

This example, albeit a bit contrived, shows what may happen if TaskScheduler.Current is TaskScheduler.FromCurrentSynchronizationContext(), i.e., made from AspNetSynchronizationContext. It doesn't use any blocking code and would have been executed smoothly in WinForms or WPF.

AspNetSynchronizationContext 的这种行为不同于 v4.0 实现(它仍然作为 LegacyAspNetSynchronizationContext).

This behavior of AspNetSynchronizationContext is different from the v4.0 implementation (which is still there as LegacyAspNetSynchronizationContext).

那么,这种变化的原因是什么?我想,这背后的想法可能是为了减少死锁的差距,但是当使用 Task.Wait()Task.Result.

So, what is the reason for such change? I thought, the idea behind this might be to reduce the gap for deadlocks, but deadlock are still possible with the current implementation, when using Task.Wait() or Task.Result.

IMO,这样说更合适:

IMO, it'd more appropriate to put it like this:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),
    TaskContinuationOptions.ExecuteSynchronously);
_lastScheduledTask = newTask;

或者,至少,我希望它使用 TaskScheduler.Default 而不是 TaskScheduler.Current.

Or, at least, I'd expect it to use TaskScheduler.Default rather than TaskScheduler.Current.

如果我在 web.config 中使用 <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false"/> 启用 LegacyAspNetSynchronizationContext>,它按预期工作:同步上下文安装在等待任务结束的线程上,并在那里同步执行继续.

If I enable LegacyAspNetSynchronizationContext with <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" /> in web.config, it works as desired: the synchronization context gets installed on the thread where the awaited task has ended, and the continuation is synchronously executed there.

推荐答案

现在我的猜测是,他们已经通过这种方式实现了 AspNetSynchronizationContext.Post 以避免可能导致堆栈溢出的无限递归的可能性.如果从传递给 Post 本身的回调中调用 Post,则可能会发生这种情况.

Now my guess is, they have implemented AspNetSynchronizationContext.Post this way to avoid a possibility of infinite recursion which might lead to stack overflow. That might happen if Post is called from the callback passed to Post itself.

不过,我认为额外的线程切换可能太贵了.本来可以这样避免的:

Still, I think an extra thread switch might be too expensive for this. It could have been possibly avoided like this:

var sameStackFrame = true
try
{
    //TODO: also use TaskScheduler.Default rather than TaskScheduler.Current 
    Task newTask = _lastScheduledTask.ContinueWith(completedTask => 
    {
        if (sameStackFrame) // avoid potential recursion
           return completedTask.ContinueWith(_ => SafeWrapCallback(action));
        else 
        {
           SafeWrapCallback(action);
           return completedTask;
        }
    }, TaskContinuationOptions.ExecuteSynchronously).Unwrap();

    _lastScheduledTask = newTask;    
}
finally
{
    sameStackFrame = false;
}

基于这个想法,我创建了一个自定义等待器,它为我提供了所需的行为:

Based on this idea, I've created a custom awaiter which gives me the desired behavior:

await task.ConfigureContinue(synchronously: true);

如果操作在同一堆栈帧上同步完成,则使用 SynchronizationContext.Post,如果在不同的堆栈帧上完成,则使用 SynchronizationContext.Send(它甚至可以是相同的线程,在一些循环后被 ThreadPool 异步重用):

It uses SynchronizationContext.Post if operation completed synchronously on the same stack frame, and SynchronizationContext.Send if it did on a different stack frame (it could even be the same thread, asynchronously reused by ThreadPool after some cycles):

using System;
using System.Diagnostics;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;

namespace TestApp.Controllers
{
    /// <summary>
    /// TestController
    /// </summary>
    public class TestController : ApiController
    {
        public async Task<string> GetData()
        {
            Debug.WriteLine(String.Empty);

            Debug.WriteLine(new
            {
                where = "before await",
                thread = Thread.CurrentThread.ManagedThreadId,
                context = SynchronizationContext.Current
            });

            // add some state to flow
            HttpContext.Current.Items.Add("_context_key", "_contextValue");
            CallContext.LogicalSetData("_key", "_value");

            var task = Task.Delay(100).ContinueWith(t =>
            {
                Debug.WriteLine(new
                {
                    where = "inside ContinueWith",
                    thread = Thread.CurrentThread.ManagedThreadId,
                    context = SynchronizationContext.Current
                });
                // return something as we only have the generic awaiter so far
                return Type.Missing; 
            }, TaskContinuationOptions.ExecuteSynchronously);

            await task.ConfigureContinue(synchronously: true);

            Debug.WriteLine(new
            {
                logicalData = CallContext.LogicalGetData("_key"),
                contextData = HttpContext.Current.Items["_context_key"],
                where = "after await",
                thread = Thread.CurrentThread.ManagedThreadId,
                context = SynchronizationContext.Current
            });

            return "OK";
        }
    }

    /// <summary>
    /// TaskExt
    /// </summary>
    public static class TaskExt
    {
        /// <summary>
        /// ConfigureContinue - http://stackoverflow.com/q/23062154/1768303
        /// </summary>
        public static ContextAwaiter<TResult> ConfigureContinue<TResult>(this Task<TResult> @this, bool synchronously = true)
        {
            return new ContextAwaiter<TResult>(@this, synchronously);
        }

        /// <summary>
        /// ContextAwaiter
        /// TODO: non-generic version 
        /// </summary>
        public class ContextAwaiter<TResult> :
            System.Runtime.CompilerServices.ICriticalNotifyCompletion
        {
            readonly bool _synchronously;
            readonly Task<TResult> _task;

            public ContextAwaiter(Task<TResult> task, bool synchronously)
            {
                _task = task;
                _synchronously = synchronously;
            }

            // awaiter methods
            public ContextAwaiter<TResult> GetAwaiter()
            {
                return this;
            }

            public bool IsCompleted
            {
                get { return _task.IsCompleted; }
            }

            public TResult GetResult()
            {
                return _task.Result;
            }

            // ICriticalNotifyCompletion
            public void OnCompleted(Action continuation)
            {
                UnsafeOnCompleted(continuation);
            }

            // Why UnsafeOnCompleted? http://blogs.msdn.com/b/pfxteam/archive/2012/02/29/10274035.aspx
            public void UnsafeOnCompleted(Action continuation)
            {
                var syncContext = SynchronizationContext.Current;
                var sameStackFrame = true; 
                try
                {
                    _task.ContinueWith(_ => 
                    {
                        if (null != syncContext)
                        {
                            // async if the same stack frame
                            if (sameStackFrame)
                                syncContext.Post(__ => continuation(), null);
                            else
                                syncContext.Send(__ => continuation(), null);
                        }
                        else
                        {
                            continuation();
                        }
                    }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
                }
                finally
                {
                    sameStackFrame = false;
                }
            }
        }
    }
}

这篇关于AspNetSynchronizationContext 并等待 ASP.NET 中的延续的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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