阅读时间过长。使用Task.ConfigureAwait(continueOnCapturedContext: false)
可能会引入冗余线程切换。我正在寻找一致的解决方案。
长版本。 ConfigureAwait(false)
背后的主要设计目标是尽可能减少SynchronizationContext.Post
的冗余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);
}
}
现在,让我们说一些客户端代码在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)
引起。这是因为ConfigureAwait(false)
pushes the continuation to ThreadPool
来自具有同步上下文的线程(在本例中是UI线程)。
在这种特殊情况下,MethodAsync
最好没有ConfigureAwait(false)
。然后它只需要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)
并且有the best practices的所有良好意图,并且她对SomeAsyncApi
的内部实施一无所知。 如果ConfigureAwait(false)
被使用&#34;一直&#34; (也就是SomeAsyncApi
内部),这不会有问题,但是&#39她超出了她的控制范围。
它与WindowsFormsSynchronizationContext
(或DispatcherSynchronizationContext
)的关系如何,我们可能根本不关心额外的线程切换。但是,类似的情况可能发生在ASP.NET中,其中AspNetSynchronizationContext.Post
实际上是这样做的:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
整个事情可能看起来像一个人为的问题,但我确实看到了很多这样的生产代码,包括客户端和服务器端。我遇到的另一个可疑模式:await TaskCompletionSource.Task.ConfigureAwait(false)
与SetResult
在与前await
捕获的同步上下文中被调用。同样,延续被冗余地推送到ThreadPool
。这种模式背后的原因是&#34;它有助于避免死锁&#34;。
问题:根据ConfigureAwait(false)
所描述的行为,我正在寻找一种优雅的方式来使用async/await
同时仍然最小化冗余线程/上下文切换。理想情况下,可以使用现有的第三方库。
到目前为止我看过的内容:
使用async
卸载Task.Run
lambda并不理想,因为它引入了至少一个额外的线程切换(尽管它可以节省许多其他线程):
await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);
另一个hackish解决方案可能是暂时从当前线程中删除同步上下文,因此它不会被内部调用链中的任何后续等待捕获(我之前提到它{{3} }):
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;
}
这有效,但我不喜欢它篡改线程的当前同步上下文,尽管范围非常短。此外,还有另一个含义:当前线程中没有SynchronizationContext
时,环境TaskScheduler.Current
将用于await
延续。为了解释这一点,WithNoContext
可能会像下面一样被改变,这会使这种黑客更具异国情调:
// task = func();
var task2 = new Task<Task<TResult>>(() => func());
task2.RunSynchronously(TaskScheduler.Default);
task = task2.Unwrap();
我很欣赏其他任何想法。
已更新,以解决here:
我会说它是相反的,因为正如斯蒂芬所说的那样 他的回答&#34; ConfigureAwait(false)的目的不是诱导a 线程切换(如果需要),而是防止过多的代码 在特定的特殊环境中运行。&#34;你不同意和 是合规的根源。
由于你的答案已被编辑,@i3arnon's comment我不同意,为清楚起见:
ConfigureAwait(false)目标是尽可能减少工作量 &#34;特别&#34; (例如UI)线程需要尽管线程处理 它需要切换。
我也不同意你的here is your statement声明。我会将您推荐给主要来源,Stephen Toub的current version:
避免不必要的封送
如果可能,请确保您正在调用的异步实现 不需要被阻塞的线程来完成操作 (这样,你可以使用普通的阻塞机制来等待 同步以便在其他地方完成异步工作)。在里面 async / await的情况,这通常意味着确保任何等待 在你正在调用的异步实现中使用 所有等待点上的ConfigureAwait(false);这将阻止等待 从尝试编组回到当前的SynchronizationContext。如 一个库实现者,这是一个永远使用的最佳实践 在所有等待中配置等待(假),除非你有 具体原因没有;这不仅有助于避免这些 各种各样的死锁问题,,但也有性能,因为它避免了 不必要的编组费用。
它的确表示目标是避免不必要的编组成本,以提高性能。线程切换(流动ExecutionContext
,除其他外) 是一个很大的编组费用。
现在,它并没有说任何地方的目标是减少在特殊情况下完成的工作量。线程或上下文。
虽然这可能对UI线程有一定意义,但我仍然认为它不是ConfigureAwait
背后的主要目标。还有其他 - 更结构化的 - 最小化UI线程工作的方法,比如使用await Task.Run(work)
块。
此外,最小化AspNetSynchronizationContext
上的工作是没有意义的 - 它本身就是从线程流向线程,与UI线程不同。相反,一旦你AspNetSynchronizationContext
,你想做尽可能多的工作,以避免在处理HTTP请求的过程中不必要的切换。尽管如此,在ASP.NET中使用ConfigureAwait(false)
仍然是完全合理的:如果使用得当,它会再次减少服务器端的线程切换。
答案 0 :(得分:16)
当您处理异步操作时,线程切换的开销太小而无法关注(一般来说)。 ConfigureAwait(false)
的目的不是引发线程切换(如果需要),而是防止在特定特殊上下文中运行太多代码。
这种模式背后的原因是&#34;它有助于避免死锁&#34;。
叠加潜水。
但我确实认为这在一般情况下是无问题的。当我遇到没有正确使用ConfigureAwait
的代码时,我只需将其包裹在Task.Run
中并继续前进。线程切换的开销并不值得担心。
答案 1 :(得分:7)
ConfigureAwait(false)背后的主要设计目标是尽可能减少用于等待的冗余SynchronizationContext.Post连续回调。这通常意味着更少的线程切换和更少的UI线程工作。
我不同意你的前提。 ConfigureAwait(false)
目标是尽可能地减少需要编组的工作,特别是&#34; (例如,用户界面)上下文的线程切换可能需要关闭该上下文。
如果目标是减少线程切换,则可以在所有工作中保持相同的特殊上下文,然后不需要其他线程。
要实现这一点,您应该使用ConfigureAwait
无处不在,您不必关心执行延续的线程。如果你采用你的例子并适当地使用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<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 }
现在,在您关注延续线程的情况下(例如,当您使用UI控件时),您需要支付&#34;付费&#34;通过切换到该线程,通过将相关工作发布到该线程。你仍然从所有不需要该线程的工作中获益。
如果您想更进一步并从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
的工作开销非常微不足道。
答案 2 :(得分:1)
显然,内置 map
的行为是在 <>
{options.map((el, i) =>
<OptionsFly key={`${el.title}_${i}`} title={el.title} text={el.text} />
)}
</>
上调用 ConfigureAwait(false)
的延续。这样做的原因 I assume 是为了防止出现多个异步工作流正在等待同一个未完成任务,然后在同一线程上以序列化方式调用它们的延续的情况。这种情况可能会导致死锁,以防一个工作流的继续被阻塞,并等待来自另一个工作流的信号。另一个工作流永远不会有机会发送信号,因为它的延续将位于同一(阻塞)线程的等待队列中。
如果您不希望在您的应用程序中出现这种情况(如果您确定一个任务永远不会被两个工作流等待),那么您可以尝试使用下面的自定义 await
方法:>
ThreadPool
在您的示例中(在方法 ConfigureAwait2
中),我将 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);
}
}
替换为 .ConfigureAwait(false)
,并且得到了以下输出:
.ConfigureAwait2(false)