c# – AspNetSynchronizationContext并等待ASP.NET中的延续

前端之家收集整理的这篇文章主要介绍了c# – AspNetSynchronizationContext并等待ASP.NET中的延续前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
在异步ASP.NET Web API控制器方法等待之后,我注意到了一个意想不到的(我会说,一个冗余的)线程切换.

例如,下面我会期望在#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中的ThreadPoolTask​​Scheduler实例(但不一定是这样,见下文).

我可以使用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.但是,直到有一个实际(非设计)的方案来支持这一点,球队不会再倾向于进一步调查.

猜你在找的C#相关文章