AspNetSynchronizationContext 并等待 ASP.NET 中的延续

本文关键字:延续 NET ASP 等待 AspNetSynchronizationContext | 更新日期: 2023-09-27 17:58:18

我注意到在异步 ASP.NET Web API 控制器方法中await后出现了意外的(我会说是冗余的(线程切换。

例如,下面我希望在位置 #2 和 3# 看到相同的ManagedThreadId,但大多数情况下我在 #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的实现,基本上可以归结为:

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

因此,延续被安排在ThreadPool,而不是内联。在这里,ContinueWith使用 TaskScheduler.Current ,根据我的经验,这始终是 ASP.NET 内部ThreadPoolTaskScheduler的实例(但不一定是这样,见下文(。

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

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

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中顺利执行。

AspNetSynchronizationContext的这种行为与 v4.0 实现不同(v4.0 实现仍然存在LegacyAspNetSynchronizationContext(。

那么,这种变化的原因是什么呢?我想,这背后的想法可能是减少死锁的差距,但是当使用Task.Wait()Task.Result时,当前的实现仍然可以使用死锁。

IMO,这样说更合适:

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

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

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

AspNetSynchronizationContext 并等待 ASP.NET 中的延续

延续被调度到新线程而不是内联是有意的。 让我们分解一下:

  1. 您正在调用 Task.Delay(100(。 100 毫秒后,基础任务将转换为已完成状态。 但是这种转换将发生在任意线程池/IOCP线程上;它不会发生在 ASP.NET 同步上下文下的线程上。

  2. 这。ContinueWith(..., ExecuteSyncally( 将导致 Debug.WriteLine(2( 发生在将 Task.Delay(100( 转换为终端状态的线程上。 继续使用本身将返回一个新任务。

  3. 您正在等待 [2] 返回的任务。 由于完成任务 [2] 的线程不受 ASP.NET 同步上下文的控制,因此 async/await 机制将调用 SynchronizationContext.Post。 此方法始终约定为异步调度。

async/await 机制确实有一些优化,可以在完成线程上内联执行延续,而不是调用 SynchronizationContext.Post,但仅当完成线程当前在其即将调度到的同步上下文下运行时,该优化才会启动。 上面的示例中不是这种情况,因为 [2] 在任意线程池线程上运行,但它需要调度回 AspNetSynchronizationContext 以运行 [3] 延续。 这也解释了为什么如果使用 .ConfigureAwait(false(:[3] 延续可以在 [2] 中内联,因为它将在默认同步上下文下调度。

对于您的其他问题:Task.Wait(( 和 Task.Result,新的同步上下文并非旨在减少相对于 .NET 4.0 的死锁条件。 (事实上,在新同步上下文中出现死锁比在旧上下文中稍微容易一些。 新的同步上下文旨在实现 .Post(( 与 async/await 机制配合得很好,旧的同步上下文在这样做时惨遭失败。 (旧同步上下文的实现 .Post(( 是阻塞调用线程,直到同步原语可用,然后内联调度回调。

在未知已完成的任务上调用请求线程调用 Task.Wait(( 和 Task.Result

仍会导致死锁,就像在 Win Forms 或 WPF 应用程序中从 UI 线程调用 Task.Wait(( 或 Task.Result 一样。

最后,Task.Factory.StartNew的怪异可能是一个实际的错误。 但是,除非有一个实际的(非人为的(场景来支持这一点,否则团队不会倾向于进一步调查这一点。

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

不过,我认为额外的线程开关可能太贵了。它本可以像这样避免:

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;
}

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

await task.ConfigureContinue(synchronously: true);

如果操作在同一堆栈帧上同步完成,它使用SynchronizationContext.Post,如果在不同的堆栈帧上完成,则SynchronizationContext.Send使用(它甚至可以是同一个线程,在几个周期后由ThreadPool异步重用(:

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;
                }
            }
        }
    }
}