【问题标题】:AspNetSynchronizationContext and await continuations in ASP.NETAspNetSynchronizationContext 并在 ASP.NET 中等待继续
【发布时间】:2014-05-28 12:49:53
【问题描述】:

我注意到在异步 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 中 ThreadPoolTaskScheduler 的一个实例.NET(但不一定是这样,见下文)。

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

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 实现不同(它仍然作为LegacyAspNetSynchronizationContext 存在)。

那么,这种变化的原因是什么?我想,这背后的想法可能是为了减少死锁的差距,但是在使用@987654341时,当前的实现仍然可能出现死锁@ 或 Task.Result

IMO,这样说更合适:

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

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

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

【问题讨论】:

    标签: c# asp.net .net task-parallel-library async-await


    【解决方案1】:

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

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

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

    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 中的 UI 线程调用 Task.Wait() 或 Task.Result或 WPF 应用程序。

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

    【讨论】:

    • 我认为SynchronizationContext.Post 没有严格的合同总是在线程不同上执行回调i> 到调用者的线程。我不会那样对待MSDN docs。例如,我有一个 gist hereCurrentThreadSyncContext 实现为 AspNetSynchronizationContext 的包装器,这为我提供了 await 延续所需的确切行为,它对我的​​测试用例非常有用。
    • 关于这一点:“.Post() 的旧同步上下文的实现是阻塞调用线程,直到同步原语可用,然后内联调度回调。” -我不确定你在这里指的是什么同步原语。如果我在web.config 中启用LegacyAspNetSynchronizationContext&lt;add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" /&gt;,它将按预期工作:同步。上下文被安装并且继续发生在同一个线程上。
    • SynchronizationContext.Send 和 SynchronizationContext.Post 大致仿照 Windows UI 消息泵中的 SendMessage / PostMessage。因此 Post 应该异步调度并立即返回。旧的同步上下文锁定在 HttpApplication 实例上,甚至在 Post 内部,这可能导致对 Post 的调用被阻塞(eek!)。旧的同步上下文是一个非常糟糕的设计示例。新的同步上下文并不完美,但它满足了使 async / await 工作所需的所有合同,这实际上是我们试图完成的所有工作。 :)
    【解决方案2】:

    现在我的猜测是,他们以这种方式实现了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;
                    }
                }
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-05-07
      • 1970-01-01
      • 2016-08-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多