我经常看到有人建议在异步库代码中使用
在我看来,一个可行的替代方案是在库的顶层公共入口点将当前同步上下文设置为null,并忘记
在库入口点上简单地将当前同步上下文设置为null是否有问题?除了await发布到默认同步上下文可能会导致微不足道的性能损失之外,还有什么潜在的问题吗?
(编辑#1)添加一些我所指的示例代码:
执行此操作将输出以下内容:
这一切都像我预期的那样运作,但我没有遇到有人建议库在内部执行此操作。我发现要求每个内部等待点都调用
(编辑#2)
根据 Alexei 的答案反馈,似乎我没有考虑到任务可能不会立即被等待的情况。由于执行上下文是在 await 时捕获的(而不是异步调用时),这意味着对
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 的答案反馈,似乎我没有考虑到任务可能不会立即被等待的情况。由于执行上下文是在 await 时捕获的(而不是异步调用时),这意味着对
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();
}
(第三次编辑)
基于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
返回时),那么这个问题会更好一些...我还怀疑当你试着编写这样的代码时,你会得到你的答案 :) - Alexei LevenkovSynchronizationContext.Current
,则它将始终为空(除非上下文本身做些什么来恢复它)。 - Daniel Tabuencanull
),而不是恢复它,因此在调用你的库后,所有与你的库无关的其他调用都将使用null
上下文(除非调用者显式地使用await
保护他们的上下文,这不是必需的)。 - Alexei LevenkovSynchronizationContext
在调用点被捕获,因此我的库的调用者仍然控制着同步上下文,他们的await
将使用该上下文来执行从库代码返回后的继续操作。我只在我的库中修改了SynchronizationContext
。 - Daniel Tabuencaasync
调用立即被await
,但事实并非如此。也就是说,以并行方式运行代码的标准方法是首先收集任务,然后使用WhenAll
进行await
。 - Alexei Levenkov