【问题标题】:await not using current SynchronizationContext等待不使用当前 SynchronizationContext
【发布时间】:2014-10-07 02:06:33
【问题描述】:

在异步函数内部使用与外部不同的 SynchronizationContext 时,我的行为会令人困惑。

我的大部分程序代码都使用自定义 SynchronizationContext,它只是将 SendOrPostCallbacks 排队并在我的主线程中的特定已知点调用它们。我在开始时设置了这个自定义 SynchronizationContext,当我只使用这个时一切正常。

我遇到的问题是我希望它们的等待继续在线程池中运行的函数。

void BeginningOfTime() {
    // MyCustomContext queues each endOrPostCallback and runs them all at a known point in the main thread.
    SynchronizationContext.SetSynchronizationContext( new MyCustomContext() ); 


    // ... later on in the code, wait on something, and it should continue inside 
    // the main thread where MyCustomContext runs everything that it has queued
    int x = await SomeOtherFunction();
    WeShouldBeInTheMainThreadNow(); // ********* this should run in the main thread
}

async int SomeOtherFunction() {
    // Set a null SynchronizationContext because this function wants its continuations 
    // to run in the thread pool.
    SynchronizationContext prevContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext( null );

    try {

        // I want the continuation for this to be posted to a thread pool 
        // thread, not MyCustomContext.
        await Blah();

        WeShouldBeInAThreadPoolThread(); // ********* this should run in a thread pool thread

    } finally {
        // Restore the previous SetSynchronizationContext.
        SynchronizationContext.SetSynchronizationContext( prevContext );
    }
}

我得到的行为是每次等待之后的代码都在看似随机的线程中执行。有时,WeShouldBeInTheMainThreadNow() 在线程池线程中运行,有时在主线程中运行。有时 WeShouldBeInAThreadPoolThread() 正在运行

我在这里看不到任何模式,但我认为无论 SynchronizationContext.Current 在您使用 await 的那一行设置为什么,都将定义 await 之后的代码将在何处执行。这是一个不正确的假设吗?如果是这样,是否有一种紧凑的方法来做我在这里尝试做的事情?

【问题讨论】:

  • 如果在您执行await 时任务(或其他可等待的)实际上已完成,则无法保证会发生任何线程切换 - 请记住,代码可能只是继续运行点。

标签: c# .net synchronizationcontext


【解决方案1】:

我希望您的代码能够正常工作,但有几个可能的原因导致它不能工作:

  1. 确保您的 SynchronizationContext 在执行其延续时是最新的。
  2. 没有严格定义SynchronizationContext 被捕获。
  3. SynchronizationContext 中运行代码的正常方法是建立当前的一对一方法,然后运行依赖于它的另一个(可能是异步的)方法。
  4. 避免当前SynchronizationContext 的常规方法是将ConfigureAwait(false) 附加到所有等待的任务中。

【讨论】:

    【解决方案2】:

    关于await 有一个常见的误解,即以某种方式调用async 实现的函数会被特殊对待。

    然而,await 关键字作用于一个对象,它根本不关心等待对象来自哪里。

    也就是说,你总是可以用var blahTask = Blah(); await blahTask;重写await Blah();

    那么当你以这种方式重写外部 await 调用时会发生什么?

    // Synchronization Context leads to main thread;
    Task<int> xTask = SomeOtherFunction();
    // Synchronization Context has already been set 
    // to null by SomeOtherFunction!
    int x = await xTask;
    

    然后,还有另一个问题:内部方法中的finally 在延续中执行,这意味着它是在线程池上执行的——所以不仅你取消了SynchronizationContext,而且你的@ 987654330@ 将(可能)在未来某个时间在另一个线程上恢复。但是,因为我不太了解SynchronizationContext 的流动方式,所以很可能SynchronizationContext 根本没有恢复,它只是设置在另一个线程上(记住SynchronizationContext.Current 是线程-本地...)

    这两个问题结合起来很容易解释您观察到的随机性。 (也就是说,您正在从多个线程操作准全局状态...)

    问题的根源在于await 关键字不允许调度延续任务。

    通常,您只需指定“await 之后的代码与await 之前的代码在同一上下文中并不重要”,在这种情况下,使用ConfigureAwait(false) 将是适当的;

    async Task SomeOtherFunction() {
        await Blah().ConfigureAwait(false);
    }
    

    但是,如果您绝对想指定“我希望await 之后的代码在线程池上运行” - 这是应该很少见,那么您不能这样做await,但你可以做到,例如使用 ContinueWith - 但是,您将混合使用 Task 对象的多种方式,这可能会导致代码非常混乱。

    Task SomeOtherFunction() {
        return Blah()
            .ContinueWith(blahTask => WeShouldBeInAThreadPoolThread(),
                          TaskScheduler.Default);
    }
    

    【讨论】:

    • 关于finally 在线程池上下文中执行的观点很好,但我必须强烈反对ContinueWith 的建议。 ConfigureAwait(false) 就足够了。
    • @StephenCleary:我不知道Blah() 在哪里执行。如果Blah() 在线程池上执行,那么你是对的。但如果不是(例如,如果它是一个 I/O 任务,那么我猜该任务将在 I/O 完成端口上完成 - 但无论哪种方式都不能保证),那么继续不会被安排在线程池。然而,对我来说真正令人困惑的是为什么需要控制 continuation 任务的执行位置。
    • 常见的概念模型是代码要么在特定上下文中运行(在本例中为自定义 SynchronizationContext),要么在“其他地方”运行。 “其他地方”是常规线程池线程还是 IOCP 线程池线程几乎无关紧要。
    • @StephenCleary:是的,我同意。我会在这个意义上添加一个编辑。
    • @JeanHominal: 我想允许一组特定的延续在线程池中运行,因为它们没有必要等待MyCustomContext 在主线程中运行它们(它会只是减慢步骤的顺序)。您(和@StephenCleary 的) .ConfigureAwait(false) 解决方案效果很好,谢谢!
    猜你喜欢
    • 2021-06-20
    • 2012-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多