【发布时间】:2014-01-06 02:59:35
【问题描述】:
这会有点长,所以请多多包涵。
我在想默认任务调度程序(ThreadPoolTaskScheduler)的行为与默认的“ThreadPool”SynchronizationContext 的行为非常相似(后者可以通过await 隐式引用或通过@ 显式引用987654331@)。他们都安排任务在随机的ThreadPool 线程上执行。事实上,SynchronizationContext.Post 只是调用了ThreadPool.QueueUserWorkItem。
但是,当在默认SynchronizationContext 上排队的任务中使用时,TaskCompletionSource.SetResult 的工作方式存在细微但重要的区别。这是一个简单的控制台应用程序来说明它:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleTcs
{
class Program
{
static async Task TcsTest(TaskScheduler taskScheduler)
{
var tcs = new TaskCompletionSource<bool>();
var task = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
tcs.SetResult(true);
Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
},
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler);
Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await tcs.Task.ConfigureAwait(true);
Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await task.ConfigureAwait(true);
Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId);
}
// Main
static void Main(string[] args)
{
// SynchronizationContext.Current is null
// install default SynchronizationContext on the thread
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
// use TaskScheduler.Default for Task.Factory.StartNew
Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.Default).Wait();
// use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew
Console.WriteLine("\nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait();
Console.WriteLine("\nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();
}
}
}
输出:
测试 #1,线程:9 在等待 tcs.Task 之前,线程:9 在 tcs.SetResult 之前,线程:10 在等待 tcs.Task 之后,线程:10 在 tcs.SetResult 之后,线程:10 在等待任务之后,线程:10 测试#2,线程:9 在等待 tcs.Task 之前,线程:9 在 tcs.SetResult 之前,线程:10 在 tcs.SetResult 之后,线程:10 在等待 tcs.Task 之后,线程:11 在等待任务之后,线程:11 按回车退出,线程:9这是一个控制台应用程序,它的Main 线程默认没有任何同步上下文,所以我在开始运行测试之前明确安装了默认的:SynchronizationContext.SetSynchronizationContext(new SynchronizationContext())。
最初,我以为我完全理解了测试#1 期间的执行工作流程(其中任务被安排为TaskScheduler.Default)。 tcs.SetResult 同步调用第一个继续部分(await tcs.Task),然后执行点返回到tcs.SetResult 并一直同步继续,包括第二个await task。这对我来说确实有意义,直到我意识到以下内容。由于我们现在在执行await tcs.Task 的线程上安装了默认同步上下文,因此应该捕获它并且应该异步进行继续(即,在由SynchronizationContext.Post 排队的不同池线程上) .以此类推,如果我从 WinForms 应用程序中运行测试 #1,它将在 await tcs.Task 之后异步继续,在消息循环的未来迭代中 WinFormsSynchronizationContext。
但这不是测试#1 中发生的情况。出于好奇,我将ConfigureAwait(true) 更改为ConfigureAwait(false),这对输出没有有任何影响。我正在寻找对此的解释。
现在,在测试#2 期间(使用TaskScheduler.FromCurrentSynchronizationContext() 安排任务),与#1 相比,确实多了一个线程切换。从输出中可以看出,tcs.SetResult 触发的await tcs.Task 延续确实在另一个池线程上异步发生。我也试过ConfigureAwait(false),也没有任何改变。我还尝试在开始测试#2 之前立即安装SynchronizationContext,而不是在开始时安装。这也导致了完全相同的输出。
我实际上更喜欢测试#2 的行为,因为它为tcs.SetResult 触发的同步延续可能导致的副作用(以及潜在的死锁)留下了较小的间隙,即使它出现在额外线程开关的价格。但是,我不完全理解为什么不管ConfigureAwait(false)如何都会发生这种线程切换。
我熟悉以下有关该主题的优秀资源,但我仍在寻找对测试 #1 和 #2 中所见行为的良好解释。 有人可以详细说明一下吗?
The Nature of TaskCompletionSource
Parallel Programming: Task Schedulers and Synchronization Context
Parallel Programming: TaskScheduler.FromCurrentSynchronizationContext
It's All About the SynchronizationContext
[UPDATE] 我的意思是,默认同步上下文对象已显式安装在主线程上,在测试#1 中线程命中第一个await tcs.Task 之前。 IMO,它不是 GUI 同步上下文这一事实并不意味着它不应该被捕获以在await 之后继续。这就是为什么我希望tcs.SetResult 之后的继续发生在与ThreadPool 不同的线程上(由SynchronizationContext.Post 排队),而主线程可能仍被TcsTest(...).Wait() 阻塞。这与one described here 非常相似。
所以我继续实现了一个哑同步上下文类TestSyncContext,它只是SynchronizationContext 的一个包装器。现在已安装它而不是 SynchronizationContext 本身:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleTcs
{
public class TestSyncContext : SynchronizationContext
{
public override void Post(SendOrPostCallback d, object state)
{
Console.WriteLine("TestSyncContext.Post, thread: " + Thread.CurrentThread.ManagedThreadId);
base.Post(d, state);
}
public override void Send(SendOrPostCallback d, object state)
{
Console.WriteLine("TestSyncContext.Send, thread: " + Thread.CurrentThread.ManagedThreadId);
base.Send(d, state);
}
};
class Program
{
static async Task TcsTest(TaskScheduler taskScheduler)
{
var tcs = new TaskCompletionSource<bool>();
var task = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("before tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
tcs.SetResult(true);
Console.WriteLine("after tcs.SetResult, thread: " + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
},
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler);
Console.WriteLine("before await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await tcs.Task.ConfigureAwait(true);
Console.WriteLine("after await tcs.Task, thread: " + Thread.CurrentThread.ManagedThreadId);
await task.ConfigureAwait(true);
Console.WriteLine("after await task, thread: " + Thread.CurrentThread.ManagedThreadId);
}
// Main
static void Main(string[] args)
{
// SynchronizationContext.Current is null
// install default SynchronizationContext on the thread
SynchronizationContext.SetSynchronizationContext(new TestSyncContext());
// use TaskScheduler.Default for Task.Factory.StartNew
Console.WriteLine("Test #1, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.Default).Wait();
// use TaskScheduler.FromCurrentSynchronizationContext() for Task.Factory.StartNew
Console.WriteLine("\nTest #2, thread: " + Thread.CurrentThread.ManagedThreadId);
TcsTest(TaskScheduler.FromCurrentSynchronizationContext()).Wait();
Console.WriteLine("\nPress enter to exit, thread: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();
}
}
}
神奇的是,事情发生了更好的变化!这是新的输出:
测试#1,线程:10 在等待 tcs.Task 之前,线程:10 在 tcs.SetResult 之前,线程:6 TestSyncContext.Post,线程:6 在 tcs.SetResult 之后,线程:6 在等待 tcs.Task 之后,线程:11 在等待任务之后,线程:6 测试#2,线程:10 TestSyncContext.Post,线程:10 在等待 tcs.Task 之前,线程:10 在 tcs.SetResult 之前,线程:11 TestSyncContext.Post,线程:11 在 tcs.SetResult 之后,线程:11 在等待 tcs.Task 之后,线程:12 在等待任务之后,线程:12 按回车退出,线程数:10现在测试 #1 的行为符合预期(await tcs.Task 异步排队到池线程)。 #2似乎也可以。让我们把ConfigureAwait(true)改成ConfigureAwait(false):
测试 #1 仍按预期正确运行:ConfigureAwait(false) 使 await tcs.Task 忽略同步上下文(TestSyncContext.Post 调用已消失),因此现在它在 tcs.SetResult 之后继续同步。
为什么这与使用默认SynchronizationContext 时的情况不同?我仍然很想知道。或许,默认的任务调度器(负责await的延续)会检查线程同步上下文的运行时类型信息,并对SynchronizationContext做一些特殊处理?
现在,我仍然无法解释测试 #2 在 ConfigureAwait(false) 时的行为。这是少了一个TestSyncContext.Post 电话,这是可以理解的。然而,await tcs.Task 仍然在与tcs.SetResult 不同的线程上继续运行(与#1 不同),这不是我所期望的。我仍在寻找这样做的原因。
【问题讨论】:
标签: c# .net multithreading task-parallel-library async-await