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

标签 c# async-await task-parallel-library

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

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

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

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

(编辑#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() 也会在整个应用程序中造成麻烦。这似乎可以通过一行代码简单地在库的公共(public)入口点解决问题。我错过了什么?

(编辑#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.
    }

最佳答案

I often see recommended for async library code, that we should use ConfigureAwait(false) on all async calls to avoid situations where the return of our call will be scheduled on a UI thread or a web request synchronization context causing issues with deadlocks among other things.

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

It seems to me that a viable alternative is to simply set the current synchronization context to null at the top-level public-facing entry points of the library, and just forget about ConfigureAwait(false).

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

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

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

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

However, I don't see many instances of people taking or recommending this approach.

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

using (SynchronizationContextSwitcher.NoContext())
{
  ...
}

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

Are there any potential problems with this approach (other than the possible insignificant performance hit of having the await post to the default synchronization context)?

实际上,这是微不足道的性能 yield

关于c# - ConfigureAwait(false) 与将同步上下文设置为 null,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41776399/

相关文章:

c# - 我在实现异步 RelayCommand 时犯了什么错误?

exception-handling - 如何在不抛出 TaskCanceledExceptions 的情况下等待任务?

c# - .NET 中在单独(单个)线程上管理任务队列的最佳方式

.net - .NET任务计划程序支持多少个处理器核心

javascript - C# 裁剪、缩放、旋转图像

c# - 区分本质上是Int32的多种类型

javascript - 如何以同步方式执行剪贴板复制、窗口打开、事件处理程序和 setInterval?

c# - 查看 Windows Defender 磁盘扫描是否正在运行

c# - 使用过时版本的 MyAssembley.XmlSerializers.dll

mysql - 在 Nodejs 路由中执行多个查询