再访问Task.ConfigureAwait(continueOnCapturedContext:false)
太久阅读了。 使用Task.ConfigureAwait(continueOnCapturedContext: false)
可能会引入冗余线程切换。 我正在寻找一致的解决方案。
长版。 ConfigureAwait(false)
背后的主要设计目标是尽可能减少用于await
冗余SynchronizationContext.Post
连续回调。 这通常意味着更少的线程切换和更少的UI线程工作。 但是,它并不总是如何运作。
例如,有一个实现SomeAsyncApi
API的第三方库。 请注意,由于某些原因,此库中的任何位置都不使用ConfigureAwait(false)
:
// some library, SomeClass class public static async Task 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"); }
输出:
{step = A1,thread = 9} {step = B1,thread = 9} {step = X1,thread = 9} {step = X1.5,thread = 11} {step = X2,thread = 9} {step = B2,thread = 11} {step = A2,thread = 9}
这里,逻辑执行流程通过4个线程切换。 其中2个是冗余的,由SomeAsyncApi().ConfigureAwait(false)
引起SomeAsyncApi().ConfigureAwait(false)
。 这是因为ConfigureAwait(false)
从具有同步上下文的线程(在本例中为UI线程) 将连续推送到ThreadPool
。
在这种特殊情况下, MethodAsync
没有ConfigureAwait(false)
, MethodAsync
会更好 。 然后它只需要2个线程开关和4个:
{step = A1,thread = 9} {step = B1,thread = 9} {step = X1,thread = 9} {step = X1.5,thread = 11} {step = X2,thread = 9} {step = B2,thread = 9} {step = A2,thread = 9}
但是, MethodAsync
的作者使用了ConfigureAwait(false)
,并且遵循了最佳实践 ,并且她对SomeAsyncApi
内部实现SomeAsyncApi
。 如果“一直”使用ConfigureAwait(false)
(即, SomeAsyncApi
), 这不会是一个问题 ,但这超出了她的控制范围。
这就是它与WindowsFormsSynchronizationContext
(或DispatcherSynchronizationContext
)的关系,我们可能根本不关心额外的线程切换。 但是,类似的情况可能发生在ASP.NET中,其中AspNetSynchronizationContext.Post
实际上是这样做的:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;
整个事情可能看起来像一个人为的问题,但我确实看到了很多这样的生产代码,包括客户端和服务器端。 我遇到的另一个可疑模式: await TaskCompletionSource.Task.ConfigureAwait(false)
,并且在与前一个await
捕获的同步上下文中调用SetResult
。 同样,延续被冗余地推送到ThreadPool
。 这种模式背后的原因是“它有助于避免死锁”。
问题 :根据所描述的ConfigureAwait(false)
行为ConfigureAwait(false)
,我正在寻找一种优雅的方式来使用async/await
同时仍然最小化冗余线程/上下文切换。 理想情况下,可以使用现有的第三方库。
到目前为止我看过的内容 :
-
使用
Task.Run
卸载async
lambda并不理想,因为它引入了至少一个额外的线程切换(尽管它可以保存许多其他线程切换):await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);
-
另一个hackish解决方案可能是暂时从当前线程中删除同步上下文,因此它不会被内部调用链中的任何后续等待捕获(我之前在此处提到过 ):
async Task MethodAsync() { TaskExt.Log("B1"); await TaskExt.WithNoContext(() => SomeAsyncApi()).ConfigureAwait(false); TaskExt.Log("B2"); }
{step = A1,thread = 8} {step = B1,thread = 8} {step = X1,thread = 8} {step = X1.5,thread = 10} {step = X2,thread = 10} {step = B2,thread = 10} {step = A2,thread = 8}
public static Task WithNoContext(Func<Task> func) { Task 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
可能会像下面一样被改变,这会使这个黑客更具异国情调:// task = func(); var task2 = new Task<Task>(() => func()); task2.RunSynchronously(TaskScheduler.Default); task = task2.Unwrap();
我很欣赏其他任何想法。
更新 ,以解决@ i3arnon的评论 :
我会说这是另一种方式,因为斯蒂芬在他的回答中说:“ConfigureAwait(false)的目的不是诱导线程切换(如果需要),而是防止在特定的特殊上下文上运行太多的代码。 “ 您不同意并且是您的合规的根源。
由于你的答案已被编辑,为了清楚起见, 这是我不同意的陈述 :
ConfigureAwait(false)目标是尽可能地减少“特殊”(例如UI)线程需要处理的工作,尽管它需要线程切换。
我也不同意你对该声明的当前版本 。 我将把你推荐给主要来源,Stephen Toub的博客文章 :
避免不必要的封送
如果可能的话,请确保您正在调用的异步实现不需要被阻塞的线程来完成操作(这样,您可以使用常规阻塞机制同步等待异步工作在其他地方完成)。 在async / await的情况下,这通常意味着确保您正在调用的异步实现中的任何等待都在所有等待点上使用ConfigureAwait(false); 这将阻止await尝试编组回当前的SynchronizationContext。 作为一个库实现者,最好总是在所有等待中使用ConfigureAwait(false),除非你有特殊的理由不这样做; 这不仅有助于避免这些类型的死锁问题, 而且还有助于避免性能,因为它避免了不必要的封送成本。
它的确表示,目标是避免不必要的编组成本,以提高性能 。 线程切换(流动ExecutionContext
等) 是一个很大的编组成本。
现在,它没有说任何目标是减少在“特殊”线程或上下文上完成的工作量。
虽然这可能对UI线程有一定意义,但我仍然认为它不是ConfigureAwait
背后的主要目标。 还有其他 – 更结构化的 – 最小化UI线程工作的方法,比如使用await Task.Run(work)
块。
而且,最小化AspNetSynchronizationContext
工作是没有意义的 – 它本身就是从一个线程流向线程,而不像UI线程。 相反, 一旦您使用AspNetSynchronizationContext
,您希望尽可能多地工作 ,以避免在处理HTTP请求的过程中进行不必要的切换。 尽管如此,在ASP.NET中使用ConfigureAwait(false)
仍然是完全合理的:如果使用正确,它又会减少服务器端的线程切换。
当您处理异步操作时,线程切换的开销太小而无法关注(一般来说)。 ConfigureAwait(false)
目的不是引发线程切换(如果需要),而是防止在特定特殊上下文上运行太多代码。
这种模式背后的原因是“它有助于避免死锁”。
并叠加潜水。
但我确实认为这在一般情况下是无问题的。 当我遇到没有正确使用ConfigureAwait
代码时,我只需将其包装在Task.Run
并继续。 线程切换的开销不值得担心。
ConfigureAwait(false)背后的主要设计目标是尽可能减少用于等待的冗余SynchronizationContext.Post连续回调。 这通常意味着更少的线程切换和更少的UI线程工作。
我不同意你的前提。 ConfigureAwait(false)
目标是尽可能地减少需要编组回“特殊”(例如UI)上下文的工作,尽管它可能需要关闭该上下文的线程切换。
如果目标是减少线程切换,则可以在所有工作中保持相同的特殊上下文,然后不需要其他线程。
为了实现这一点,您应该在任何地方使用ConfigureAwait
,而不关心执行continuation的线程。 如果您采用您的示例并适当地使用ConfigureAwait
您将只获得一个开关(而不是没有它的2):
private async void Button_Click(object sender, RoutedEventArgs e) { TaskExt.Log("A1"); await AnotherClass.MethodAsync().ConfigureAwait(false); TaskExt.Log("A2"); } public class AnotherClass { public static async Task MethodAsync() { TaskExt.Log("B1"); await SomeClass.SomeAsyncApi().ConfigureAwait(false); TaskExt.Log("B2"); } } public class SomeClass { public static async Task SomeAsyncApi() { TaskExt.Log("X1"); await Task.Delay(1000).WithCompletionLog(step: "X1.5").ConfigureAwait(false); TaskExt.Log("X2"); return 42; } }
输出:
{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 11 } { step = B2, thread = 11 } { step = A2, thread = 11 }
现在,在你关心延续线程的地方(例如,当你使用UI控件时),通过切换到该线程,通过将相关工作发布到该线程来“支付”。 你仍然从所有不需要该线程的工作中获益。
如果你想更进一步并从UI线程中删除这些async
方法的同步工作,你只需要使用Task.Run
一次,然后添加另一个开关:
private async void Button_Click(object sender, RoutedEventArgs e) { TaskExt.Log("A1"); await Task.Run(() => AnotherClass.MethodAsync()).ConfigureAwait(false); TaskExt.Log("A2"); }
输出:
{ step = A1, thread = 9 } { step = B1, thread = 10 } { step = X1, thread = 10 } { step = X1.5, thread = 11 } { step = X2, thread = 11 } { step = B2, thread = 11 } { step = A2, thread = 11 }
这个使用ConfigureAwait(false)
指南针对库开发人员,因为它实际上是重要的,但重点是尽可能地使用它,在这种情况下,您可以减少对这些特殊上下文的工作,同时保持最小的线程切换。
使用WithNoContext
与在任何地方使用ConfigureAwait(false)
具有完全相同的结果。 然而,缺点是它与线程的SynchronizationContext
混淆,并且你在async
方法中没有意识到这一点。 ConfigureAwait
直接影响当前的await
因此您可以同时拥有因果关系。
正如我已经指出的那样,使用Task.Run
与在任何地方使用ConfigureAwait(false)
具有完全相同的结果,具有将async
方法的同步部分卸载到ThreadPool
的附加值。 如果需要,那么Task.Run
是合适的,否则ConfigureAwait(false)
就足够了。
现在,如果你正在处理一个错误的库,当没有正确使用ConfigureAwait(false)
,你可以通过删除SynchronizationContext
来解决它,但是使用Thread.Run
更简单,更清晰,卸载工作到ThreadPool
有一个非常开销微不足道。