AspNetSynchronizationContext并等待ASP.NET中的延续

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

例如,下面我希望在#2和3#位置看到相同的ManagedThreadId ,但最常见的是我在#3处看到了不同的线程:

 public class TestController : ApiController { public async Task 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)或自定义awaiter消除这样的冗余线程切换,但这会消除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实现不同(它仍然作为LegacyAspNetSynchronizationContext )。

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

IMO,这样说更合适:

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

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

如果我在LegacyAspNetSynchronizationContext使用启用LegacyAspNetSynchronizationContext ,它将按预期工作:同步上下文安装在等待任务结束的线程上,并且继续是在那里同步执行。

将延续发送到新线程而不是内联是故意的。 让我们打破这个:

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

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

  3. 你正在等待[2]返回的任务。 由于完成Task [2]的线程不受ASP.NET同步上下文的控制,因此async / await机器将调用SynchronizationContext.Post。 该方法总是以异步方式分派。

async / await机制确实有一些优化来在完成线程上内联执行continuation而不是调用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的怪异可能是一个真正的bug。 但是,除非有一个实际的(非人为的)方案支持这一点,否则该团队不会倾向于进一步调查。

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

基于这个想法,我创建了一个自定义awaiter,它给了我所需的行为:

 await task.ConfigureContinue(synchronously: true); 

如果操作在同一堆栈帧上SynchronizationContext.Post完成,它使用SynchronizationContext.Post如果在不同的堆栈帧上执行,则使用SynchronizationContext.Post (它甚至可以是相同的线程,在一些周期后由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 { ///  /// 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; } } } } }