ConfigureAwait(false)与将同步上下文设置为null [英] ConfigureAwait(false) vs setting sync context to null

查看:48
本文介绍了ConfigureAwait(false)与将同步上下文设置为null的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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)的问题之一是,这不是您只能在库调用的入口处执行的操作.为了使其有效,必须在整个库代码中从头到尾进行整个操作.

One of the problems with using ConfigureAwait(false) is that it isn't something you can just do on the entry point of your library call. In order for it to be effective it must be done all the way down the stack throughout your library code.

在我看来,可行的替代方案是仅在库的面向公众的顶级入口点将当前同步上下文设置为null,而不必忘记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). However, I don't see many instances of people taking or recommending this approach.

仅在库入口点上将当前同步上下文设置为null是否有问题?这种方法是否存在任何潜在问题(除了将await发布到默认同步上下文可能对性能造成的影响之外)?

Is there anything wrong with simply setting the current synchronization context to null on the library entry points? 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)?

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

(EDIT #1) Adding some example code of what I mean:

   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()都可能在整个应用程序中引起麻烦.似乎只需使用一行代码就可以在库的公共入口点解决此问题.我想念什么?

This all works like I would expect, but I don't come across people recommending libraries do this internally. I find that requiring every internal await point be called with ConfigureAwait(false) is annoying, and even one missed ConfigureAwait() can cause trouble throughout an application. This seems like it would solve the issue simply at the public entry-point of the library with a single line of code. What am I missing?

(编辑#2)

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

Based on some feedback from Alexei's answer, it seems I hadn't consider the possibility of a task not being immediately awaited. Since the execution context is captured at the time of the await (not the time of the async call), that would mean the change to SynchronizationContext.Current would not be isolated to the library method. Based on this it would seem that it should suffice to force a capture of the context by wrapping the internal logic of the library in a call that forces an wait. For example:

    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.

Based on the discussion on Stephen Cleary's answer. There are some problems with this approach. But we can do a similar approach by wrapping the library call in a non-async method that still returns a task, but takes care of resetting the syncrhonization context at the end. (Note this uses the SynchronizationContextSwitcher from Stephen's AsyncEx library.

    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.
    }

推荐答案

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

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)可以防止死锁,但这 不是 其预期目的.

I recommend ConfigureAwait(false) because it (correctly) notes that the calling context is not required. It also gives you a small performance benefit. While ConfigureAwait(false) can prevent deadlocks, that is not its intended purpose.

在我看来,可行的选择是将当前同步上下文在库的面向公众的顶级入口点简单地设置为null,而无需考虑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会尝试如果没有当前的SynchronizationContext ,则在TaskScheduler.Current上恢复.

Yes, that is an option. It won't completely avoid deadlocks, though, because await will attempt to resume on TaskScheduler.Current if there's no current SynchronizationContext.

此外,用一个库替换框架级组件也感觉不对.

Also, it feels wrong to have a library replacing a framework-level component.

但是您可以根据需要执行此操作.只是不要忘记最后将其设置回其原始值.

But you can do this if you want. Just don't forget to set it back to its original value at the end.

哦,还有另一个陷阱:那里有API,可以假定当前为该框架提供的SyncCtx.某些ASP.NET帮助器API就是这样.因此,如果您回叫最终用户代码,则可能是一个问题.但是在那种情况下,您应该显式记录无论如何都要调用它们的回调的上下文.

Oh, one other pitfall: there are APIs out there that will assume the current SyncCtx is what's provided for that framework. Some ASP.NET helper APIs are like that. So, if you call back end-user code, then that could be a problem. But in that case, you should explicitly document what context their callbacks are invoked in anyway.

但是,我看不到有人采用或推荐这种方法的情况.

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

它正逐渐变得越来越流行.足够了,所以我添加了一个API为此,请在我的AsyncEx库中:

It is slowly becoming more popular. Enough so that I've added an API for this in my AsyncEx library:

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

不过,我自己还没有使用过这种技术.

I haven't used this technique myself, though.

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

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)?

实际上,这是微不足道的表现 .

Actually, it's an insignificant performance gain.

这篇关于ConfigureAwait(false)与将同步上下文设置为null的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆