在所有库代码中禁用捕获上下文,ConfigureAwait(false)
本文关键字:上下文 ConfigureAwait false 代码 | 更新日期: 2023-09-27 18:10:09
当使用await
时,默认情况下捕获SynchronizationContext
(如果存在),并且使用该上下文执行await
(延续块)之后的代码块(这会导致线程上下文切换)。
public async Task DoSomethingAsync()
{
// We are on a thread that has a SynchronizationContext here.
await DoSomethingElseAsync();
// We are back on the same thread as before here
//(well sometimes, depending on how the captured SynchronizationContext is implemented)
}
虽然这个默认值在你想在异步操作完成后回到UI线程的UI上下文中可能是有意义的,但作为大多数其他场景的默认值似乎没有意义。对于内部库代码来说,这当然没有意义,因为
- 它带来了不必要的线程上下文切换的开销。
- 很容易意外地产生死锁(如这里或这里所述)。
在我看来微软的默认设置是错误的。
现在我的问题是:
是否有其他(最好更好)的方法来解决这个问题,而不是在我的代码中与.ConfigureAwait(false)
混淆所有await
调用?这很容易被忘记,并使代码可读性降低。
更新:在每个方法的开始调用await Task.Yield().ConfigureAwait(false);
是否足够?如果这将保证我之后将在一个没有SynchronizationContext
的线程上,那么所有随后的await
调用将不会捕获任何上下文。
首先,await Task.Yield().ConfigureAwait(false)
不能工作,因为Yield
不返回Task
。还有其他跳转到池线程的方法,但它们的使用也不推荐,检查"为什么"SwitchTo"从异步CTP/发布中删除?"
如果您仍然想这样做,这里有一个很好的技巧:如果原始线程上有同步上下文,即使这里没有异步,
ConfigureAwait(false)
也会将延续推到池线程:
static Task SwitchAsync()
{
if (SynchronizationContext.Current == null)
return Task.FromResult(false); // optimize
var tcs = new TaskCompletionSource<bool>();
Func<Task> yield = async () =>
await tcs.Task.ConfigureAwait(false);
var task = yield();
tcs.SetResult(false);
return task;
}
// ...
public async Task DoSomethingAsync()
{
// We are on a thread that has a SynchronizationContext here.
await SwitchAsync().ConfigureAwait(false);
// We're on a thread pool thread without a SynchronizationContext
await DoSomethingElseAsync(); // no need for ConfigureAwait(false) here
// ...
}
再次强调,这不是我自己会广泛使用的。我对使用ConfigureAwait(false)
也有类似的担忧。一个结果是,虽然ConfigureAwait(false)
可能不是普遍完美的,但只要您不关心同步上下文,就可以将其与await
一起使用。这是。net源代码本身严格遵循的准则。
另一个结果是,如果您担心DoSomethingElseAsync
内部的第三方代码可能无法正确使用ConfigureAwait(false)
,只需执行以下操作:
public async Task DoSomethingAsync()
{
// We are on a thread that has a SynchronizationContext here.
await Task.Run(() => DoSomethingElseAsync()).ConfigureAwait(false);
// We're on a thread pool thread without a SynchronizationContext
await DoYetSomethingElseAsync(); // no need for ConfigureAwait(false) here
// ...
}
这将使用Task.Run
重写,它接受Func<Task>
lambda,在池线程上运行它并返回一个未包装的任务。您只会对DoSomethingAsync
中的第一个await
执行此操作。潜在的成本与SwitchAsync
相同:一个额外的线程切换,但代码更具可读性和更好的结构化。这就是我在工作中使用的方法。
是否有更好的方法来解决这个问题,而不是在我的代码中与。configureawait (false)混淆所有等待调用?这很容易忘记,使代码可读性降低。
没有。没有任何"开箱即用"的开关,你可以打开它来改变这种行为。有ConfigureAwaiter Checker ReSharper扩展可以提供帮助。另一种选择是滚动您自己的自定义扩展方法或SynchronizationContext
包装器,它可以切换上下文,或者甚至是自定义侍者。