[英]ConfigureAwait(false) - Does the continuation always run on different thread?
[英]ConfigureAwait pushes the continuation to a pool thread
这是一些 WinForms 代码:
async void Form1_Load(object sender, EventArgs e)
{
// on the UI thread
Debug.WriteLine(new { where = "before",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
var tcs = new TaskCompletionSource<bool>();
this.BeginInvoke(new MethodInvoker(() => tcs.SetResult(true)));
await tcs.Task.ContinueWith(t => {
// still on the UI thread
Debug.WriteLine(new { where = "ContinueWith",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);
// on a pool thread
Debug.WriteLine(new { where = "after",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
}
输出:
{ where = before, ManagedThreadId = 10, IsThreadPoolThread = False } { where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False } { where = after, ManagedThreadId = 11, IsThreadPoolThread = True }
为什么 ConfigureAwait 会主动将await
继续推送到这里的池线程?
我用“推到池中的线程”在这里当主延续回调(的描述的情况下action
参数TaskAwaiter.UnsafeOnCompleted
被调用在一个线程,而二次回调(一传给ConfiguredTaskAwaiter.UnsafeOnCompleted
)排队到一个池线程。
文档说:
continueOnCapturedContext ... true 尝试将延续编组回捕获的原始上下文; 否则为假。
我知道当前线程上安装了WinFormsSynchronizationContext
。 尽管如此,没有尝试进行编组,执行点已经存在。
因此,它更像是“永远不要继续捕获的原始上下文” ......
正如预期的那样,如果执行点已经在没有同步上下文的池线程上,则没有线程切换:
await Task.Delay(100).ContinueWith(t =>
{
// on a pool thread
Debug.WriteLine(new { where = "ContinueWith",
Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);
{ where = before, ManagedThreadId = 10, IsThreadPoolThread = False } { where = ContinueWith, ManagedThreadId = 6, IsThreadPoolThread = True } { where = after, ManagedThreadId = 6, IsThreadPoolThread = True }
已更新,再进行一项测试以查看是否有任何同步。 上下文不足以继续(而不是原始的)。 确实是这样:
class DumbSyncContext: SynchronizationContext
{
}
// ...
Debug.WriteLine(new { where = "before",
Thread.CurrentThread.ManagedThreadId,
Thread.CurrentThread.IsThreadPoolThread });
var tcs = new TaskCompletionSource<bool>();
var thread = new Thread(() =>
{
Debug.WriteLine(new { where = "new Thread",
Thread.CurrentThread.ManagedThreadId,
Thread.CurrentThread.IsThreadPoolThread});
SynchronizationContext.SetSynchronizationContext(new DumbSyncContext());
tcs.SetResult(true);
Thread.Sleep(1000);
});
thread.Start();
await tcs.Task.ContinueWith(t => {
Debug.WriteLine(new { where = "ContinueWith",
Thread.CurrentThread.ManagedThreadId,
Thread.CurrentThread.IsThreadPoolThread});
}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);
Debug.WriteLine(new { where = "after",
Thread.CurrentThread.ManagedThreadId,
Thread.CurrentThread.IsThreadPoolThread });
{ where = before, ManagedThreadId = 9, IsThreadPoolThread = False } { where = new Thread, ManagedThreadId = 10, IsThreadPoolThread = False } { where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False } { where = after, ManagedThreadId = 6, IsThreadPoolThread = True }
为什么 ConfigureAwait 在这里主动将 await 继续推送到池线程?
它不会“将它推送到线程池线程”,而是说“不要强迫自己回到之前的SynchronizationContext
”。
如果您不捕获现有上下文,则处理该await
之后的代码的延续将只在线程池线程上运行,因为没有要编组回的上下文。
现在,这与“推送到线程池”略有不同,因为当您执行ConfigureAwait(false)
时,不能保证它会在线程池上运行。 如果你打电话:
await FooAsync().ConfigureAwait(false);
FooAsync()
可能会同步执行,在这种情况下,您将永远不会离开当前上下文。 在这种情况下, ConfigureAwait(false)
没有实际效果,因为由await
功能创建的状态机将短路并直接运行。
如果您想看到这一点,请创建一个像这样的异步方法:
static Task FooAsync(bool runSync)
{
if (!runSync)
await Task.Delay(100);
}
如果你这样称呼它:
await FooAsync(true).ConfigureAwait(false);
您将看到您停留在主线程上(假设它是等待之前的当前上下文),因为在代码路径中没有实际执行的异步代码。 与FooAsync(false).ConfigureAwait(false);
相同的调用FooAsync(false).ConfigureAwait(false);
但是,会导致它在执行后跳转到线程池线程。
这是基于挖掘.NET Reference Source对这种行为的解释。
如果使用ConfigureAwait(true)
,则继续通过使用SynchronizationContextTaskScheduler
TaskSchedulerAwaitTaskContinuation
完成,在这种情况下一切都很清楚。
如果使用ConfigureAwait(false)
(或者如果没有同步上下文要捕获),则通过AwaitTaskContinuation
完成,它首先尝试内联延续任务,然后在无法内联时使用ThreadPool
将其排队。
内联由IsValidLocationForInlining
确定,它从不使用自定义同步上下文将任务内联到线程上。 然而,它最好将它内联到当前池线程上。 这解释了为什么我们在第一种情况下被推送到池线程上,而在第二种情况下(使用Task.Delay(100)
)保持在同一个池线程上。
我认为最容易以稍微不同的方式思考这一点。
假设你有:
await task.ConfigureAwait(false);
首先,如果task
已经完成,那么正如 Reed 指出的那样,实际上会忽略ConfigureAwait
并继续执行(同步,在同一线程上)。
否则, await
将暂停该方法。 在这种情况下,当await
恢复并看到ConfigureAwait
为false
,有特殊的逻辑来检查代码是否具有SynchronizationContext
,如果是,则在线程池上恢复。 这是无证但并非不当行为。 因为它是无证的,我建议你不要依赖于行为; 如果你想在线程池上运行一些东西,使用Task.Run
。 ConfigureAwait(false)
字面意思是“我不在乎这个方法在什么上下文中恢复。”
请注意, ConfigureAwait(true)
(默认值)将在当前SynchronizationContext
或TaskScheduler
上继续该方法。 虽然ConfigureAwait(false)
将在任何线程上继续该方法,除了具有SynchronizationContext
线程。 它们并不完全相反。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.