我经常看到建议用于异步库代码,我们应该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 TaskGetStringFromMyLibInternal() { SynchronizationContext.SetSynchronizationContext(null); await Task.Delay(1000); return "HELLO WORLD"; } async Task 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 TaskGetStringFromMyLibInternal() { SynchronizationContext.SetSynchronizationContext(null); await Task.Delay(1000); return "HELLO WORLD"; } Task GetStringFromMyLibAsync() { using (SynchronizationContextSwitcher.NoContext()) { return GetStringFromMyLibInternal(); } //Context will be restored by the time this method returns its task. }
Stephen Clea.. 9
我经常看到建议用于异步库代码,我们应该在所有异步调用上使用ConfigureAwait(false),以避免在UI线程或Web请求同步上下文中调度返回我们的调用,导致死锁等问题.
我建议ConfigureAwait(false)
因为它(正确)注意到不需要调用上下文.它还为您提供了一个小的性能优势.虽然ConfigureAwait(false)
可以防止死锁,但这不是它的预期目的.
在我看来,一个可行的替代方案是在库的顶层面向公众的入口点简单地将当前同步上下文设置为null,并且忘记ConfigureAwait(false).
是的,这是一个选择.但是,它不会完全避免死锁,因为如果没有电流,await
它将尝试继续TaskScheduler.Current
SynchronizationContext
.
另外,让库替换框架级组件感觉不对.
但如果你愿意,你可以这样做.只是不要忘记在最后将其设置回原始值.
哦,另一个陷阱:那里有API会假设当前的SyncCtx是为该框架提供的.一些ASP.NET助手API就是这样.因此,如果您回调最终用户代码,那么这可能是个问题.但在这种情况下,您应该明确记录其回调调用的上下文.
但是,我没有看到很多人采取或推荐这种方法.
它正逐渐变得更受欢迎.够了,所以我在我的AsyncEx库中添加了一个API:
using (SynchronizationContextSwitcher.NoContext()) { ... }
不过,我自己并没有使用过这种技术.
这种方法是否存在任何潜在的问题(除了将等待发布到默认同步上下文可能无关紧要的性能影响)?
其实,这是一个微不足道的性能增益.
我经常看到建议用于异步库代码,我们应该在所有异步调用上使用ConfigureAwait(false),以避免在UI线程或Web请求同步上下文中调度返回我们的调用,导致死锁等问题.
我建议ConfigureAwait(false)
因为它(正确)注意到不需要调用上下文.它还为您提供了一个小的性能优势.虽然ConfigureAwait(false)
可以防止死锁,但这不是它的预期目的.
在我看来,一个可行的替代方案是在库的顶层面向公众的入口点简单地将当前同步上下文设置为null,并且忘记ConfigureAwait(false).
是的,这是一个选择.但是,它不会完全避免死锁,因为如果没有电流,await
它将尝试继续TaskScheduler.Current
SynchronizationContext
.
另外,让库替换框架级组件感觉不对.
但如果你愿意,你可以这样做.只是不要忘记在最后将其设置回原始值.
哦,另一个陷阱:那里有API会假设当前的SyncCtx是为该框架提供的.一些ASP.NET助手API就是这样.因此,如果您回调最终用户代码,那么这可能是个问题.但在这种情况下,您应该明确记录其回调调用的上下文.
但是,我没有看到很多人采取或推荐这种方法.
它正逐渐变得更受欢迎.够了,所以我在我的AsyncEx库中添加了一个API:
using (SynchronizationContextSwitcher.NoContext()) { ... }
不过,我自己并没有使用过这种技术.
这种方法是否存在任何潜在的问题(除了将等待发布到默认同步上下文可能无关紧要的性能影响)?
其实,这是一个微不足道的性能增益.