AspNetSynchronizationContext and await continuations in ASP.NET

前端 未结 2 556
名媛妹妹
名媛妹妹 2020-12-03 00:15

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

For example, below

2条回答
  •  有刺的猬
    2020-12-03 00:52

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

    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
    {
        /// 
        /// TestController
        /// 
        public class TestController : ApiController
        {
            public async Task 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";
            }
        }
    
        /// 
        /// TaskExt
        /// 
        public static class TaskExt
        {
            /// 
            /// ConfigureContinue - http://stackoverflow.com/q/23062154/1768303
            /// 
            public static ContextAwaiter ConfigureContinue(this Task @this, bool synchronously = true)
            {
                return new ContextAwaiter(@this, synchronously);
            }
    
            /// 
            /// ContextAwaiter
            /// TODO: non-generic version 
            /// 
            public class ContextAwaiter :
                System.Runtime.CompilerServices.ICriticalNotifyCompletion
            {
                readonly bool _synchronously;
                readonly Task _task;
    
                public ContextAwaiter(Task task, bool synchronously)
                {
                    _task = task;
                    _synchronously = synchronously;
                }
    
                // awaiter methods
                public ContextAwaiter 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;
                    }
                }
            }
        }
    }
    

提交回复
热议问题