【发布时间】:2017-06-06 04:40:43
【问题描述】:
我经常看到推荐用于异步库代码,我们应该在所有异步调用上使用ConfigureAwait(false),以避免我们的调用的返回将被安排在 UI 线程或 Web 请求同步上下文中导致死锁问题的情况其他的东西。
使用ConfigureAwait(false) 的一个问题是,这不是您可以在库调用的入口点执行的操作。为了使其有效,它必须在整个库代码的整个堆栈中一直完成。
在我看来,一个可行的替代方法是在库的面向公众的顶级入口点将当前同步上下文简单地设置为 null,而忘记 ConfigureAwait(false)。但是,我没有看到很多人采用或推荐这种方法。
在库入口点上简单地将当前同步上下文设置为 null 有什么问题吗?这种方法是否存在任何潜在问题(除了将 await 发布到默认同步上下文可能对性能造成微不足道的影响)?
(编辑#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)
根据 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.
}
【问题讨论】:
-
如果你能证明 正确 设置和恢复上下文(特别是在你的库方法中代码作为
await的一部分返回时)这个问题会好得多...另外我怀疑你会在你写完这样的代码时得到你的答案:) -
我不确定你的意思。据我了解,同步上下文已被捕获,但未在等待点恢复。它只是被等待者用来发布延续委托,但如果你在等待之后立即执行
SynchronizationContext.Current,它将始终为空(除非上下文本身做了一些事情来恢复自己)。 -
您确实了解您的提议看起来像是您想要更改当前线程的同步上下文(即从 UI 到
null)并且不恢复它,因此不会使所有 other 调用与您的库相关,以便在调用您的库后使用null上下文(除非调用者使用await明确保护他们的上下文,这不是必需的)。 -
我用显示我的意思的示例代码更新了我的问题。希望它更清楚。我想得越多,我就越看不到它的不利之处(甚至是性能方面的不利因素)。但我希望有比我更有经验的人在我更大规模地使用这种方法之前对其进行验证。
-
我添加了代码作为答案 - 您似乎希望每个
async呼叫都会立即成为await-ed,但事实并非如此。 IE。并行运行代码的标准方法是先收集任务,然后再收集任务await和 hWhenAll。
标签: c# async-await task-parallel-library