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

时间:2017-01-21 06:26:55

标签: 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()也会导致整个应用程序出现问题。这似乎只是在图书馆的公共入口点用一行代码来解决问题。我错过了什么?

(编辑#2)

根据阿列克谢回答的一些反馈,似乎我没有考虑到没有立即等待任务的可能性。由于执行上下文是在等待时(而不是异步调用的时间)捕获的,这意味着对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的回答的讨论。这种方法存在一些问题。但是我们可以通过将库调用包装在仍然返回任务的非异步方法中来执行类似的方法,但是最后会负责重置syncrhonization上下文。 (注意,这使用了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.
    }

2 个答案:

答案 0 :(得分:8)

  

我经常看到建议使用异步库代码,我们应该对所有异步调用使用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())
{
  ...
}

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

  

此方法是否存在任何潜在问题(除了将等待发布到默认同步上下文后可能无关紧要的性能影响)?

实际上,这是一项微不足道的收益

答案 1 :(得分:1)

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

我不相信你可以安全地在库函数中设置当前线程的同步上下文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;
}