C# 重新访问Task.ConfigureAwait(continueOnCapturedContext:false)
太长,无法读取。使用C# 重新访问Task.ConfigureAwait(continueOnCapturedContext:false),c#,.net,task-parallel-library,async-await,C#,.net,Task Parallel Library,Async Await,太长,无法读取。使用Task.ConfigureAwait(continueOnCapturedContext:false)可能会引入冗余线程切换。我正在寻找一个一致的解决方案 长版本。ConfigureAwait(false)背后的主要设计目标是减少冗余的SynchronizationContext。如果可能,发布await的后续回调。这通常意味着更少的线程切换和更少的UI线程工作。然而,这并不总是它的工作原理 例如,有一个第三方库实现了SomeAsyncApiAPI。请注意,由于某些原因,C
Task.ConfigureAwait(continueOnCapturedContext:false)
可能会引入冗余线程切换。我正在寻找一个一致的解决方案
长版本。ConfigureAwait(false)背后的主要设计目标是减少冗余的SynchronizationContext。如果可能,发布await
的后续回调。这通常意味着更少的线程切换和更少的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);
}
}
输出:
{ 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 }
然而,MethodAsync
的作者使用了ConfigureAwait(false)
,出于良好的意图和遵循,她对SomeAsyncApi
的内部实现一无所知如果一直使用ConfigureAwait(false)
(即在SomeAsyncApi
内部),这不会是一个问题,但这超出了她的控制范围
这就是windowsformsssynchronizationcontext
(或dispatcherssynchronizationcontext
)的情况,我们可能根本不关心额外的线程切换。但是,在ASP.NET中也可能发生类似的情况,其实质是:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
整个过程看起来可能是人为的,但我确实看到了很多这样的生产代码,包括客户端和服务器端。我遇到的另一个有问题的模式是:await TaskCompletionSource.Task.ConfigureAwait(false)
,其中SetResult
在为前者捕获的同步上下文上被调用。继续操作再次被冗余地推送到ThreadPool
。这种模式背后的理由是“它有助于避免死锁”
问题:根据所描述的ConfigureAwait(false)
行为,我正在寻找一种优雅的方式来使用async/await
,同时尽量减少冗余线程/上下文切换。理想情况下,可以使用现有的第三方库
到目前为止,我所看到的:
- 使用
Task.Run卸载async
lambda并不理想,因为它至少引入了一个额外的线程开关(尽管它可能会节省许多其他线程开关):
- 另一个黑客解决方案可能是暂时从当前线程中删除同步上下文,这样它就不会被内部调用链中的任何后续等待捕获(我之前提到过它):
如果还有其他想法,我将不胜感激
更新,以解决:
我会说这是另一种方式,因为正如斯蒂芬在书中所说
他的回答是“配置等待(错误)的目的不是诱导
线程切换(如有必要),而是为了防止代码过多
在特定环境下运行。”您不同意,并且
是您的兼容的根
由于您的答案已被编辑,为了清楚起见,我不同意:
ConfigureWait(false)的目标是尽可能减少工作量
“特殊”(例如UI)线程需要处理,而不管线程是什么
它需要的开关
我也不同意你对这一说法的看法。我将向您介绍主要来源Stephen Toub的:
避免不必要的编组
如果可能的话,请确保调用的是异步实现
不需要阻塞线程来完成操作
(这样,您就可以使用普通的阻塞机制来等待
同步进行异步工作,以便在其他位置完成)。在
在异步/等待的情况下,这通常意味着确保
在您正在调用的异步实现中,您正在使用
在所有等待点上配置等待(false);这将阻止等待
从尝试封送回当前SynchronizationContext。作为
作为库实现者,始终使用它是最佳实践
在所有等待上配置等待(false),除非您有
不同意的具体理由;这不仅有助于避免这些问题
各种死锁问题,但也会影响性能,因为它可以避免
不必要的编组成本。
它确实指出,目标是为了提高性能,避免不必要的编组成本。线程切换(在ExecutionContext
中流动)是一个巨大的封送成本
现在,它没有说目标是减少在“特殊”线程或上下文上完成的工作量
虽然这对UI线程来说可能有一定意义,但我仍然不认为这是ConfigureAwait
背后的主要目标。还有其他更结构化的方法来最小化UI线程上的工作,比如使用大块的wait Task.Run(work)
此外,将AspNetSynchronizationContext
上的工作最小化是毫无意义的,这与UI线程不同,AspNetSynchronizationContext本身从一个线程流向另一个线程。恰恰相反,一旦你在代码> ASPNETSimultIsActudio<<代码>中,你就要尽可能多地工作,以避免在处理HTTP请求过程中不必要的切换。尽管如此,在ASP.NET中使用ConfigureAwait(false)
仍然非常有意义:如果使用正确,它会再次减少服务器端线程切换
ConfigureAwait(false)背后的主要设计目标是减少冗余的SynchronizationContext。尽可能对await进行后续回调。这通常意味着更少的线程切换和更少的UI线程工作
我不同意你的前提<代码>配置等待(false)
目标是尽可能减少
{ 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 }
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);
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<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;
}
// task = func();
var task2 = new Task<Task<TResult>>(() => func());
task2.RunSynchronously(TaskScheduler.Default);
task = task2.Unwrap();
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<int> 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 }
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 }
public static ConfiguredTaskAwaitable2 ConfigureAwait2(this Task task,
bool continueOnCapturedContext)
=> new ConfiguredTaskAwaitable2(task, continueOnCapturedContext);
public struct ConfiguredTaskAwaitable2 : INotifyCompletion
{
private readonly Task _task;
private readonly bool _continueOnCapturedContext;
public ConfiguredTaskAwaitable2(Task task, bool continueOnCapturedContext)
{
_task = task; _continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredTaskAwaitable2 GetAwaiter() => this;
public bool IsCompleted { get { return _task.IsCompleted; } }
public void GetResult() { _task.GetAwaiter().GetResult(); }
public void OnCompleted(Action continuation)
{
var capturedContext = _continueOnCapturedContext ?
SynchronizationContext.Current : null;
_ = _task.ContinueWith(_ =>
{
if (capturedContext != null)
capturedContext.Post(_ => continuation(), null);
else
continuation();
}, default, TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}