例如,下面我会期望在#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",context = SynchronizationContext.Current }); },TaskContinuationOptions.ExecuteSynchronously); //.ConfigureAwait(false); Debug.WriteLine(new { where = "3) after await",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()); },scheduler: TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();
这个例子,虽然有点设计,显示了如果TaskScheduler.Current是TaskScheduler.FromCurrentSynchronizationContext(),即由AspNetSynchronizationContext构成的可能会发生什么.它不使用任何阻止代码,并且将在WinForms或WPF中顺利执行.
AspNetSynchronizationContext的这种行为与v4.0实现不同(仍然是LegacyAspNetSynchronizationContext
).
那么这种变化的原因是什么呢?我以为,这个想法可能是为了减少死锁的差距,但是当使用Task.Wait()或Task.Result时,目前的实现仍然可能导致死锁.
海事组织,更适合这样说:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),TaskContinuationOptions.ExecuteSynchronously); _lastScheduledTask = newTask;
或者,至少我期望它使用TaskScheduler.Default而不是TaskScheduler.Current.
如果我启用LegacyAspNetSynchronizationContext与< add key =“aspnet:UseTaskFriendlySynchronizationContext”value =“false”/>在web.config中,它可以按需要工作:同步上下文被安装在等待已经完成的任务已经结束的线程上,并且继续在那里同步执行.
解决方法
你正在打电话给Task.Delay(100). 100毫秒后,基础任务将转换到完成的状态.但是,这种转换将在任意的ThreadPool / IOCP线程上发生;它不会发生在ASP.NET同步上下文下的线程上.
> .ContinueWith(…,ExecuteSynchronously)将导致Debug.WriteLine(2)发生在将Task.Delay(100)转换为终端状态的线程上. ContinueWith本身将返回一个新的Task.
你正在等待[2]返回的任务.由于完成Task [2]的线程不在ASP.NET同步上下文的控制之下,所以异步/等待机制将调用SynchronizationContext.Post.这种方法总是以异步方式签约.
异步/等待机制确实有一些优化来在完成的线程上内联执行连续性,而不是调用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.但是,直到有一个实际(非设计)的方案来支持这一点,球队不会再倾向于进一步调查.