【发布时间】:2015-04-09 05:00:42
【问题描述】:
阅读时间过长。使用Task.ConfigureAwait(continueOnCapturedContext: false) 可能会引入冗余线程切换。我正在寻找一个一致的解决方案。
长版。ConfigureAwait(false) 背后的主要设计目标是尽可能减少await 的冗余SynchronizationContext.Post 延续回调。这通常意味着更少的线程切换和更少的 UI 线程工作。然而,它并不总是这样工作的。
例如,有一个实现SomeAsyncApi API 的第三方库。请注意,由于某种原因,ConfigureAwait(false) 并未在此库中的任何地方使用:
// some library, SomeClass class
public static async Task<int> SomeAsyncApi()
{
TaskExt.Log("X1");
// await Task.Delay(1000) without ConfigureAwait(false);
// WithCompletionLog only shows the actual Task.Delay completion thread
// and doesn't change the awaiter behavior
await Task.Delay(1000).WithCompletionLog(step: "X1.5");
TaskExt.Log("X2");
return 42;
}
// logging helpers
public static partial class TaskExt
{
public static void Log(string step)
{
Debug.WriteLine(new { step, thread = Environment.CurrentManagedThreadId });
}
public static Task WithCompletionLog(this Task anteTask, string step)
{
return anteTask.ContinueWith(
_ => Log(step),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
现在,假设有一些客户端代码在 WinForms UI 线程上运行并使用 SomeAsyncApi:
// another library, AnotherClass class
public static async Task MethodAsync()
{
TaskExt.Log("B1");
await SomeClass.SomeAsyncApi().ConfigureAwait(false);
TaskExt.Log("B2");
}
// ...
// a WinFroms app
private async void Form1_Load(object sender, EventArgs e)
{
TaskExt.Log("A1");
await AnotherClass.MethodAsync();
TaskExt.Log("A2");
}
输出:
{ 步骤 = A1,线程 = 9 } { 步骤 = B1,线程 = 9 } { 步 = X1,线程 = 9 } { 步 = X1.5,线程 = 11 } { 步 = X2,线程 = 9 } { 步骤 = B2,线程 = 11 } { 步骤 = A2,线程 = 9 }这里,逻辑执行流程经过 4 个线程切换。 其中2个是多余的,由SomeAsyncApi().ConfigureAwait(false)引起。这是因为ConfigureAwait(false) pushes the continuation to ThreadPool 来自具有同步上下文的线程(在本例中为 UI 线程)。
在这种特殊情况下,MethodAsync 最好没有ConfigureAwait(false)。那么它只需要 2 个线程切换 vs 4 个:
但是,MethodAsync 的作者出于善意使用ConfigureAwait(false) 并关注the best practices,而她对SomeAsyncApi 的内部实现一无所知。 如果“一直”使用ConfigureAwait(false) 不会有问题(也就是在SomeAsyncApi 内部),但这超出了她的控制范围。
WindowsFormsSynchronizationContext(或DispatcherSynchronizationContext)就是这样,我们可能根本不关心额外的线程切换。但是,在 ASP.NET 中可能会发生类似的情况,AspNetSynchronizationContext.Post 本质上是这样做的:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
整个事情可能看起来是一个人为的问题,但我确实看到了很多这样的生产代码,包括客户端和服务器端。我遇到的另一个有问题的模式:await TaskCompletionSource.Task.ConfigureAwait(false) 和 SetResult 在与前一个 await 捕获的同步上下文中被调用。再一次,延续被多余地推送到ThreadPool。这种模式背后的原因是“它有助于避免死锁”。
问题:鉴于ConfigureAwait(false) 的描述行为,我正在寻找一种使用async/await 的优雅方式,同时仍尽量减少冗余线程/上下文切换。理想情况下,可以使用现有的 3rd 方库。
到目前为止我所看到的:
-
使用
Task.Run卸载asynclambda 并不理想,因为它引入了至少一个额外的线程切换(尽管它可能会节省许多其他线程):await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false); -
另一种骇人听闻的解决方案可能是从当前线程中临时删除同步上下文,这样它就不会被内部调用链中的任何后续等待捕获(我之前提到过它here):
{ 步骤 = A1,线程 = 8 } { 步骤 = B1,线程 = 8 } { 步 = X1,线程 = 8 } { 步 = X1.5,线程 = 10 } { 步 = X2,线程 = 10 } { 步骤 = B2,线程 = 10 } { 步骤 = A2,线程 = 8 }async Task MethodAsync() { TaskExt.Log("B1"); await TaskExt.WithNoContext(() => SomeAsyncApi()).ConfigureAwait(false); TaskExt.Log("B2"); }public static Task<TResult> WithNoContext<TResult>(Func<Task<TResult>> func) { Task<TResult> task; var sc = SynchronizationContext.Current; try { SynchronizationContext.SetSynchronizationContext(null); // do not await the task here, so the SC is restored right after // the execution point hits the first await inside func task = func(); } finally { SynchronizationContext.SetSynchronizationContext(sc); } return task; }这可行,但我不喜欢它篡改线程当前同步上下文的事实,尽管范围很短。此外,这里还有另一个含义:在当前线程上没有
SynchronizationContext的情况下,环境TaskScheduler.Current将用于await延续。考虑到这一点,WithNoContext可能会像下面这样进行更改,这将使这个 hack 更加奇特:// task = func(); var task2 = new Task<Task<TResult>>(() => func()); task2.RunSynchronously(TaskScheduler.Default); task = task2.Unwrap();
如果有任何其他想法,我将不胜感激。
更新,地址@i3arnon's comment:
我会说这是相反的,因为正如斯蒂芬在 他的回答“ConfigureAwait(false) 的目的不是诱导 线程切换(如有必要),而是为了防止代码过多 在特定的特殊上下文中运行。”您不同意并且 是您合规的根源。
由于您的答案已被编辑,here is your statement 为了清楚起见,我不同意:
ConfigureAwait(false) 目标是尽可能减少工作 尽管有线程,“特殊”(例如 UI)线程仍需要处理 它需要的开关。
我也不同意你的current version 的这种说法。我会把你推荐给主要来源,Stephen Toub 的blog post:
避免不必要的编组
如果可能,请确保您正在调用的异步实现 不需要阻塞的线程来完成操作 (这样,您可以使用正常的阻塞机制等待 同步以使异步工作在其他地方完成)。在里面 在异步/等待的情况下,这通常意味着确保任何等待 在您正在调用的异步实现内部使用 在所有等待点上配置等待(假);这将防止等待 从试图编组回到当前的 SynchronizationContext。作为 一个库实现者,最好总是使用 ConfigureAwait(false) 在你的所有等待上,除非你有一个 不这样做的具体原因;这不仅有助于避免这些 各种死锁问题,也为了性能,因为它避免了 不必要的编组成本。
它确实表示目标是为了性能而避免不必要的编组成本。线程切换(其中包括ExecutionContext)是一个很大的编组成本。
现在,它并没有说目标是减少在“特殊”线程或上下文上完成的工作量。
虽然这可能对 UI 线程有一定意义,但我仍然不认为这是 ConfigureAwait 背后的主要目标。还有其他更结构化的方法可以最小化 UI 线程上的工作,例如使用 await Task.Run(work) 块。
此外,最小化AspNetSynchronizationContext 上的工作毫无意义 - 它本身在线程之间流动,与 UI 线程不同。恰恰相反,一旦您使用AspNetSynchronizationContext,您希望尽可能多地工作,以避免在处理 HTTP 请求的过程中进行不必要的切换。尽管如此,在 ASP.NET 中使用ConfigureAwait(false) 仍然非常有意义:如果使用正确,它会再次减少服务器端线程切换。
【问题讨论】:
-
当 TPL 团队需要为 await 定义默认行为时,他们必须做出一个非常艰难的决定。只有糟糕的选择。在 GUI 应用程序中,默认情况下 await 会失败,或者默认情况下所有库都会做错事。这可能是 await 最讨厌的方面。
-
@Pingpong,在answering your question 时,我在总结上尽了最大努力。 TLTR,我对此的看法:不要使用
ConfigureAwait(false)并且 - 如果绝对必要 - 使用TaskRun(() => SomethingAsync())以希望脱离同步上下文。 -
非常有趣!我现在试图通过创建一个省略
ConfigureAwait(false)可能导致更多线程切换的场景来证明相反的情况。 :-) -
是的,我也喜欢
TaskScheduler.SwitchTo()概念的想法。顺便说一句,我放弃了尝试创建反例。这并不容易,甚至不可能。 :-)
标签: c# .net task-parallel-library async-await