【问题标题】:Why doesn't the SynchronizationContext flow here?为什么 SynchronizationContext 不在这里流动?
【发布时间】:2018-06-22 22:06:16
【问题描述】:

在我的 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

在内部 await 之后添加“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。

【问题讨论】:

  • 你运行的是什么版本的 .net ?
  • Web 应用程序配置为使用 .NET Framework v4.6.1。
  • 这让我很困惑,任何人都应该从 sn-p 获得这个问题的重现。使用 Debug > Windows > Threads 窗口来验证假设,确保您仍在预期的线程上。
  • 好的,我会简单地为你提供一个完整的实现和简单的调试输出,你可以在黑色的 ASP.NET 应用程序中自己运行。
  • 在 Visual Studio 2017 中创建一个新的空 MVC 项目,并将 HomeController.vb 替换为 this code,确实给了我您所看到的相同结果。我仍在尝试围绕 async / await 的更精细点展开思考,所以我无法真正说明为什么会发生这种情况。

标签: async-await synchronizationcontext


【解决方案1】:

最近开始学习SynchronizationContext,看了.Net Framewrok源码后发现await只是捕获当前的SynchronizationContext并将剩余的代码执行Post到上下文中,但是它并没有设置SynchronizaitonContext,所以你得到一个空值。

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

 // 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()

自定义同步上下文:

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 不再为空

【讨论】:

    【解决方案2】:

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

    那个 null 意味着继续记录被发送到线程池的主要线程,线程池将它们序列化(在队列中)但不会运行它们。他是一个懒惰的混蛋:-),他会尽其所能地委派一切。相反,当它们运行时,他会将这些连续记录发送回他自己的线程池 - 并且根本不会保证执行顺序(序列化只是为了他自己的安全并且是一个热点)。因此,在您的“内部等待之后”调试打印运行时,SynchronizationContext 再次为空,您处于未知线程中。

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

    不要忘记线程池中的线程永远存在 - 线程池的全部目的是永远不必销毁并创建新的线程对象。此外,GC 将所有线程对象视为根,这意味着它们所接触的任何东西都会成为永恒的(有些人会称之为泄漏),除非你明确地使指针无效。

    【讨论】:

      猜你喜欢
      • 2023-03-20
      • 2018-01-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-08-31
      • 2021-05-18
      • 2018-02-05
      相关资源
      最近更新 更多