为什么SynchronizationContext没有在这里流动?

时间:2018-06-22 22:06:17

标签: async-await synchronizationcontext

在我的Asp.Net WebApi控制器(框架版本4.6.1)中,我具有以下代码:

    [Route("async_test_2")]
    public async Task<IHttpActionResult> AsyncTest2()
    {
        TelemetryDebugWriter.IsTracingDisabled = true;
        var aspNetContext = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(new SynchronizationContext()); //set context while calling AsyncMethod
        var task = AsyncMethod();
        SynchronizationContext.SetSynchronizationContext(aspNetContext); //Restore AspNet context before awaiting
        DebugContext("Before outer await");
        await Task.WhenAll(new Task[] { task });
        DebugContext("After outer await");
        return Ok();
    }

    private async Task AsyncMethod()
    {
        DebugContext("Before inner await");
        await Task.Delay(2000);
        DebugContext("After inner await");
    }

    private void DebugContext(string location)
    {
        System.Diagnostics.Debug.WriteLine(location + "  ---  SyncContext: " + (SynchronizationContext.Current?.ToString() ?? "null") + "; ManagedThreadId: " + Thread.CurrentThread.ManagedThreadId);
    }

调试输出为:

Before inner await  ---  SyncContext: System.Threading.SynchronizationContext; ManagedThreadId: 6
Before outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 6
After inner await  ---  SyncContext: null; ManagedThreadId: 5
After outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 6

为什么继续“在内部等待之后”具有空的SynchronizationContext?如果我只删除对SetSynchronizationContext的调用并恢复它的调用(即,不修改上下文,保留默认的AspNetSynchronizationContext),则在任何调试输出中,该上下文都不为空。

Before inner await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
Before outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
After inner await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 8
After outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 8

在内部等待之后添加'ConfigureAwait(false)'将导致上下文在连续状态中为空。

Before inner await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
Before outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 7
After inner await  ---  SyncContext: null; ManagedThreadId: 7
After outer await  ---  SyncContext: System.Web.AspNetSynchronizationContext; ManagedThreadId: 8

因此,当AspNetSynchronizationContext处于活动状态时,它可以按预期方式工作,但默认的SynchronizationContext处于活动状态时,则不能按预期工作。在那种情况下,无论是否调用ConfigureAwait(false),它在连续中始终为null。

2 个答案:

答案 0 :(得分:0)

最近,我开始学习有关SynchronizationContext的知识,在阅读.Net Framewrok源代码之后,我发现等待只是捕获当前的SynchronizationContext并将剩余的代码执行发布到上下文中,但是它没有设置SynchronizaitonContext,所以您空了。

AspNetSynchronizationContext做一些额外的工作,以确保即使任务在不同的线程中执行,SynchronizationContext.Current也相同。您可以检查 ISyncContext。输入实现:

 // set synchronization context for the current thread to support the async pattern
        _originalSynchronizationContext = AsyncOperationManager.SynchronizationContext;
        AspNetSynchronizationContextBase aspNetSynchronizationContext = HttpContext.SyncContext;
        AsyncOperationManager.SynchronizationContext = aspNetSynchronizationContext;

AsyncOperationManger.SynchronizationContext 只是调用 SynchronizationContext.SetSynchronizationContext 方法来设置 SynchronizationContext.Current

您可以签出这个要替换的SynchronizationContext简单实现

new SynchronizationContext()

使用

new CustomSyncContext()

CustomSyncContext:

public class CustomSyncContext : SynchronizationContext
{
    private Task lastTask = Task.FromResult<object>(null);

    private object lockObj = new object();
    public override void Post(SendOrPostCallback d, object state)
    {
        Debug.WriteLine("post back from await");
        lock (lockObj)
        {

            var newTask = lastTask.ContinueWith(_=>{
                AsyncOperationManager.SynchronizationContext = this;
                d(state);
            }, TaskScheduler.Default);
            lastTask = newTask;
        }
    }

    public override SynchronizationContext CreateCopy()
    {
        return this;
    }

}

并发现 SynchronizationContext.Current 不再为空

答案 1 :(得分:0)

SynchronizationContext.Current是线程本地的。 Task.Delay在新线程上运行(Tasks自动“消除线程”的说法很危险),因此它的SynchronizationContext为null。线程池中的所有线程都为空(在此处携带真实对象是不安全的-它们永远不会死,在每次运行后您都需要非常小心的清理代码,否则您会陷入邪恶的纠缠)。

该null表示将连续记录发送到线程池的主运行区,该线程将其序列化(在队列中),但不会运行它们。他是一个懒惰的混蛋:-)委派他所能做的一切。取而代之的是,当它们的运行时间到来时,他会将这些连续记录发送回自己的线程池中-并且完全不保证执行顺序(序列化只是出于自身安全考虑,是一个热点)。因此,在您运行“内部等待之后”调试打印时,SynchronizationContext再次为null,并且您处于未知线程中。

您必须编写自己的SynchronizationContext派生并管理它的实例(无锁定)。您可以将它们分配给线程池的线程,但是您必须非常小心并检测到链中最后一个延续何时完成,因为此时必须清理。本质上,您还需要自己的线程类派生类。

不要忘记线程池中的线程永远存在-线程池的整个目的是永远不必销毁它,而后再创建新的线程对象。另外,GC将所有线程对象都视为根,这意味着除非您明确地使指针无效,否则它们所接触的对象也将成为永恒的对象(某些人会称之为泄漏对象)。