使用ConfigureAwait(false)的一个问题是,它不是您可以在库调用的入口点上执行的操作.为了使其有效,必须在整个库代码中一直向下完成.
在我看来,一个可行的替代方案是在库的顶层面向公众的入口点简单地将当前同步上下文设置为null,并且忘记ConfigureAwait(false).但是,我没有看到很多人采取或推荐这种方法.
简单地在库入口点上将当前同步上下文设置为null有什么问题吗?这种方法是否存在任何潜在的问题(除了将等待发布到默认同步上下文可能无关紧要的性能影响)?
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. }
解决方法
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)?
实际上,这是一个微不足道的性能提升.