ConfigureAwait(false) 与将同步上下文设置为 null

Posted

技术标签:

【中文标题】ConfigureAwait(false) 与将同步上下文设置为 null【英文标题】:ConfigureAwait(false) vs setting sync context to null 【发布时间】:2017-06-06 04:40:43 【问题描述】:

我经常看到推荐用于异步库代码,我们应该在所有异步调用上使用ConfigureAwait(false),以避免我们的调用的返回将被安排在 UI 线程或 Web 请求同步上下文中导致死锁问题的情况其他的东西。

使用ConfigureAwait(false) 的一个问题是,这不是您可以在库调用的入口点执行的操作。为了使其有效,它必须在整个库代码的整个堆栈中一直完成。

在我看来,一个可行的替代方法是在库的面向公众的***入口点将当前同步上下文简单地设置为 null,而忘记 ConfigureAwait(false)。但是,我没有看到很多人采用或推荐这种方法。

在库入口点上简单地将当前同步上下文设置为 null 有什么问题吗?这种方法是否存在任何潜在问题(除了将 await 发布到默认同步上下文可能对性能造成微不足道的影响)?

(编辑#1)添加一些我的意思的示例代码:

   public class Program
    
        public static void Main(string[] args)
        
            SynchronizationContext.SetSynchronizationContext(new LoggingSynchronizationContext(1));

            Console.WriteLine("Executing library code that internally clears synchronization context");
            //First try with clearing the context INSIDE the lib
            RunTest(true).Wait();
            //Here we again have the context intact
            Console.WriteLine($"After First Call Context in Main Method is SynchronizationContext.Current?.ToString()");


            Console.WriteLine("\nExecuting library code that does NOT internally clear the synchronization context");
            RunTest(false).Wait();
            //Here we again have the context intact
            Console.WriteLine($"After Second Call Context in Main Method is SynchronizationContext.Current?.ToString()");

        

        public async static Task RunTest(bool clearContext)
        
            Console.WriteLine($"Before Lib call our context is SynchronizationContext.Current?.ToString()");
            await DoSomeLibraryCode(clearContext);
            //The rest of this method will get posted to my LoggingSynchronizationContext

            //But.......
            if(SynchronizationContext.Current == null)
                //Note this will always be null regardless of whether we cleared it or not
                Console.WriteLine("We don't have a current context set after return from async/await");
            
        


        public static async Task DoSomeLibraryCode(bool shouldClearContext)
        
            if(shouldClearContext)
                SynchronizationContext.SetSynchronizationContext(null);
            
            await DelayABit();
            //The rest of this method will be invoked on the default (null) synchronization context if we elected to clear the context
            //Or it should post to the original context otherwise
            Console.WriteLine("Finishing library call");
        

        public static Task DelayABit()
        
            return Task.Delay(1000);
        

    

    public class LoggingSynchronizationContext : SynchronizationContext
    

        readonly int contextId;
        public LoggingSynchronizationContext(int contextId)
        
            this.contextId = contextId;
        
        public override void Post(SendOrPostCallback d, object state)
        
            Console.WriteLine($"POST TO Synchronization Context (ID:contextId)");
            base.Post(d, state);
        

        public override void Send(SendOrPostCallback d, object state)
        
            Console.WriteLine($"Post Synchronization Context (ID:contextId)");
            base.Send(d, state);
        

        public override string ToString()
        
            return $"Context (ID:contextId)";
        
    

执行这个会输出:

Executing library code that internally clears synchronization context
Before Lib call our context is Context (ID:1) 
Finishing library call 
POST TO Synchronization Context (ID:1)
We don't have a current context set after return from async/await
After First Call Context in Main Method is Context (ID:1)

Executing library code that does NOT internally clear the synchronization context 
Before Lib call our context is Context (ID:1) POST TO Synchronization Context (ID:1) 
Finishing library call
POST TO Synchronization Context (ID:1) 
We don't have a current context set after return from async/await
After Second Call Context in Main Method is Context (ID:1)

这一切都像我预期的那样工作,但我没有遇到有人建议图书馆在内部这样做。我发现要求使用ConfigureAwait(false) 调用每个内部等待点很烦人,即使错过ConfigureAwait() 也会在整个应用程序中造成麻烦。这似乎可以用一行代码简单地在库的公共入口点解决问题。我错过了什么?

(编辑#2)

根据 Alexei 的回答中的一些反馈,我似乎没有考虑到任务没有立即等待的可能性。由于执行上下文是在等待时(而不是异步调用时)捕获的,这意味着对 SynchronizationContext.Current 的更改不会被隔离到库方法。基于此,通过将库的内部逻辑包装在强制等待的调用中来强制捕获上下文似乎就足够了。例如:

    async void button1_Click(object sender, EventArgs e)
    
        var getStringTask = GetStringFromMyLibAsync();
        this.textBox1.Text = await getStringTask;
    

    async Task<string> GetStringFromMyLibInternal()
    
        SynchronizationContext.SetSynchronizationContext(null);
        await Task.Delay(1000);
        return "HELLO WORLD";
    

    async Task<string> GetStringFromMyLibAsync()
    
        //This forces a capture of the current execution context (before synchronization context is nulled
        //This means the caller's context should be intact upon return
        //even if not immediately awaited.
        return await GetStringFromMyLibInternal();          
    

(编辑#3)

基于对 Stephen Cleary 回答的讨论。这种方法存在一些问题。但是我们可以通过将库调用包装在仍然返回任务的非异步方法中来做类似的方法,但会在最后重置同步上下文。 (注意这里使用了 Stephen 的 AsyncEx 库中的 SynchronizationContextSwitcher。

    async void button1_Click(object sender, EventArgs e)
    
        var getStringTask = GetStringFromMyLibAsync();
        this.textBox1.Text = await getStringTask;
    

    async Task<string> GetStringFromMyLibInternal()
    
        SynchronizationContext.SetSynchronizationContext(null);
        await Task.Delay(1000);
        return "HELLO WORLD";
    

    Task<string> GetStringFromMyLibAsync()
    
        using (SynchronizationContextSwitcher.NoContext())
        
            return GetStringFromMyLibInternal();          
         
        //Context will be restored by the time this method returns its task.
    

【问题讨论】:

如果你能证明 正确 设置和恢复上下文(特别是在你的库方法中代码作为await 的一部分返回时)这个问题会好得多...另外我怀疑你会在你写完这样的代码时得到你的答案:) 我不确定你的意思。据我了解,同步上下文已被捕获,但未在等待点恢复。它只是被等待者用来发布延续委托,但如果你在等待之后立即执行SynchronizationContext.Current,它将始终为空(除非上下文本身做了一些事情来恢复自己)。 您确实了解您的提议看起来像是您想要更改当前线程的同步上下文(即从 UI 到 null)并且不恢复它,因此不会使所有 other 调用与您的库相关,以便在调用您的库后使用 null 上下文(除非调用者使用 await 明确保护他们的上下文,这不是必需的)。 我用显示我的意思的示例代码更新了我的问题。希望它更清楚。我想得越多,我就越看不到它的不利之处(甚至是性能方面的不利因素)。但我希望有比我更有经验的人在我更大规模地使用这种方法之前对其进行验证。 我添加了代码作为答案 - 您似乎希望每个 async 呼叫都会立即成为 await-ed,但事实并非如此。 IE。并行运行代码的标准方法是先收集任务,然后再收集任务 await 和 hWhenAll 【参考方案1】:

我经常看到推荐用于异步库代码,我们应该在所有异步调用上使用 ConfigureAwait(false) 以避免调用返回将被安排在 UI 线程或 Web 请求同步上下文中导致死锁问题的情况除其他外。

我推荐ConfigureAwait(false),因为它(正确地)指出调用上下文不是必需的。它还为您带来了一点性能优势。虽然ConfigureAwait(false) 可以防止死锁,但这不是它的预期目的。

在我看来,一个可行的替代方案是在库的面向公众的***入口点将当前同步上下文简单地设置为 null,而忘记 ConfigureAwait(false)。

是的,这是一个选项。不过,它不会完全避免死锁,因为await will attempt to resume on TaskScheduler.Current if there's no current SynchronizationContext

另外,让库替换框架级组件感觉不对。

但如果你愿意,你可以这样做。只是不要忘记在最后将其设置回原来的值。

哦,另一个陷阱:有一些 API 假设当前的 SyncCtx 是为该框架提供的。一些 ASP.NET 帮助程序 API 就是这样。因此,如果您回调最终用户代码,那么这可能是一个问题。但在这种情况下,您应该明确记录调用它们的回调的上下文。

但是,我没有看到很多人采用或推荐这种方法。

它正在慢慢变得越来越流行。够了,我加了an API for this in my AsyncEx library:

using (SynchronizationContextSwitcher.NoContext())

  ...

不过,我自己并没有使用过这种技术。

这种方法是否存在任何潜在问题(除了将 await 发布到默认同步上下文可能对性能造成微不足道的影响)?

实际上,这是一个微不足道的性能增益

【讨论】:

旁注:由于“调用上下文”包括当前的 ASP.Net 文化,因此大多数人没有意识到他们的代码对它的依赖程度。事实上,正确编写的代码将以独立于同步上下文的方式传递调用中的所有上下文,但这样的代码很少见...... 关于替换框架级组件的库。我从我进行的测试中了解到,如果编写得当,这个更改将在从库调用返回时自动恢复(前提是在上下文设置为 null 之前至少有一个异步/等待点(请参阅编辑 #2)关于这个问题。我对这如何工作的解释是否不正确? 这里非常有趣的替代方法:https://blogs.msdn.microsoft.com/benwilli/2017/02/07/alternative-to-configure-await-false-everywhere/ @StephenCleary 最后一条评论的更正链接:blogs.msdn.microsoft.com/benwilli/2017/02/09/… @BornToCode:是的,我的意思是如果它被设置为其他的TaskScheduler,比如one of these。【参考方案2】:

同步上下文与静态变量相似,在控制权离开您的方法之前更改它而不恢复将导致意外行为。

我不相信您可以安全地在库函数中设置当前线程的同步上下文 await 任何东西,因为据我所知,在编译器生成的代码中间恢复上下文是不可能的。

示例:

 async Task<int> MyLibraryMethodAsync()
 
    SynchronizationContext.SetSynchronizationContext(....);
    await SomeInnerMethod(); // note that method returns at this point

    // maybe restore synchronization context here...
    return 42;
 

 ...
 // code that uses library, runs on UI thread

 void async OnButtonClick(...)
 
    // <-- context here is UI-context, code running on UI thread
    Task<int> doSomething = MyLibraryMethodAsync(); 
    // <-- context here is set by MyLibraryMethod - i.e. null, code running on UI thread
    var textFromFastService = await FastAsync();
    // <-- context here is set by MyLibraryMethod, code running on pool thread (non-UI)
    textBox7.Text = textFromFastService; // fails...

    var get42 = await doSomething;

【讨论】:

非常有趣。这绝对是我没有考虑过的事情。我已经用针对这种情况的潜在解决方法更新了我的问题。

以上是关于ConfigureAwait(false) 与将同步上下文设置为 null的主要内容,如果未能解决你的问题,请参考以下文章

***请求上的 ConfigureAwait(false)

为整个项目/dll 设置 ConfigureAwait(false)

您可以在没有线程安全的情况下使用 ConfigureAwait(false) 吗?

ConfigureAwait(false) 不会使延续代码在另一个线程中运行

ConfigureAwait(false) 维护线程身份验证,但默认情况下不

为啥总是推荐在每一行上写 ConfigureAwait(false) 并且我真的需要它吗?