这会有点长,所以请耐心等待。
我认为默认任务调度程序(ThreadPoolTaskScheduler
)的行为与默认的"ThreadPool
"SynchronizationContext
的行为非常相似(后者可以通过await
隐式引用或通过TaskScheduler.FromCurrentSynchronizationContext()
显式引用)。它们都计划要在随机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之前。任务,线程:9在TCS之前。设置结果,线程:10后等待 tcs。任务,线程:10在TCS之后。设置结果,线程:等待任务后 10,线程:10 测试 #2,线程:9 在等待TCS之前。任务,线程:9在TCS之前。设置结果,线程:10 在TCS之后。设置结果,线程:10后等待 tcs。任务,线程: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 中看到的行为的良好解释。有人可以详细说明一下吗?
TaskCompletionSource
的本质 并行编程:任务计划程序和同步上下文 并行编程:TaskScheduler.FromCurrentSynchronizationContext
这一切都与同步上下文
有关
[更新]我的观点是,在线程到达测试 #1 中的第一个await tcs.Task
之前,默认同步上下文对象已显式安装在主线程上。IMO,它不是 GUI 同步上下文的事实并不意味着它不应该在await
之后被捕获以继续。这就是为什么我希望tcs.SetResult
后的延续发生在与ThreadPool
不同的线程上(按SynchronizationContext.Post
排队),而主线程可能仍被TcsTest(...).Wait()
阻止。这与此处描述的方案非常相似。
所以我继续实现了一个愚蠢的同步上下文类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之前。任务,线程:10 在TCS之前。设置结果,线程:6 TestSyncContext.Post,螺纹:6 在TCS之后。设置结果,线程:6 等待TCS之后。任务,线程:11 在等待任务之后,线程:6 测试 #2,线程:10 TestSyncContext.Post,螺纹:10 在等待TCS之前。任务,线程:10 在TCS之前。设置结果,线程:11 TestSyncContext.Post,螺纹:11 在TCS之后。设置结果,线程:11 等待TCS之后。任务,线程:12 等待任务后,线程:12按回车键退出,线程:10
现在测试 #1 现在按预期运行(await tcs.Task
异步排队到池线程)。 #2 似乎也可以。让我们将ConfigureAwait(true)
更改为ConfigureAwait(false)
:
测试 #1,线程:9 在等待TCS之前。任务,线程:9 在TCS之前。设置结果,线程:10 等待TCS之后。任务,线程:10 在TCS之后。设置结果,线程:10 等待任务后,线程:10 测试 #2,线程:9 TestSyncContext.Post,螺纹:9 在等待TCS之前。任务,线程:9 在TCS之前。设置结果,线程:11 在TCS之后。设置结果,线程:11 等待TCS之后。任务,线程:10 等待任务后,线程:10 按回车键退出,线程:9
测试 #1 仍然按预期正确运行:ConfigureAwait(false)
使await tcs.Task
忽略同步上下文(TestSyncContext.Post
调用消失),因此现在它在tcs.SetResult
后同步继续。
为什么这与使用默认SynchronizationContext
时的情况不同?我仍然很好奇知道。也许,默认任务计划程序(负责await
延续)检查线程同步上下文的运行时类型信息,并对SynchronizationContext
进行一些特殊处理?
现在,我仍然无法解释测试 #2 的行为,以便在ConfigureAwait(false)
.这是可以理解的,少了一个TestSyncContext.Post
电话。但是,await tcs.Task
仍然在与tcs.SetResult
不同的线程上继续(与#1不同),这不是我所期望的。我仍在寻找原因。
当您开始深入研究实现细节时,区分记录/可靠行为和未记录行为非常重要。此外,将SynchronizationContext.Current
设置为new SynchronizationContext()
并不真正被认为是合适的;.NET 中的某些类型将null
视为默认计划程序,而其他类型将null
或new SynchronizationContext()
视为默认计划程序。
当你await
一个不完整的Task
时,TaskAwaiter
默认捕获当前SynchronizationContext
- 除非它是null
的(或其GetType
返回typeof(SynchronizationContext)
),在这种情况下,TaskAwaiter
会捕获当前TaskScheduler
。此行为主要记录在案(GetType
子句不是 AFAIK)。但是,请注意,这描述的是TaskAwaiter
的行为,而不是TaskScheduler.Default
或TaskFactory.StartNew
。
捕获上下文(如果有)后,await
会安排继续。此延续是使用ExecuteSynchronously
安排的,如我的博客中所述(此行为未记录)。但是,请注意,ExecuteSynchronously
并不总是同步执行;特别是,如果延续具有任务计划程序,则它只会请求在当前线程上同步执行,并且任务计划程序可以选择拒绝同步执行它(也未记录)。
最后,请注意,可以请求TaskScheduler
同步执行任务,但SynchronizationContext
不能。因此,如果await
捕获自定义SynchronizationContext
,则它必须始终异步执行延续。
因此,在您的原始测试 #1 中:
StartNew
使用默认任务计划程序(在线程 10 上)启动新任务。SetResult
同步执行由await tcs.Task
设置的延续。- 在
StartNew
任务结束时,它同步执行await task
设置的延续。
在原始测试 #2 中:
StartNew
使用默认构造的同步上下文(在线程 10 上)的任务计划程序包装器启动新任务。请注意,线程 10 上的任务TaskScheduler.Current
设置为SynchronizationContextTaskScheduler
,其m_synchronizationContext
是new SynchronizationContext()
创建的实例;但是,该线程的SynchronizationContext.Current
是null
。SetResult
尝试在当前任务计划程序上同步执行await tcs.Task
延续;但是,它不能,因为SynchronizationContextTaskScheduler
看到线程 10 在需要new SynchronizationContext()
时具有null
SynchronizationContext.Current
。因此,它异步调度延续(在线程 11 上)。- 类似的情况发生在
StartNew
任务结束时;在这种情况下,我相信await task
继续在同一线程上是巧合的。
最后,我必须强调,依赖未记录的实施细节是不明智的。如果要让async
方法在线程池线程上继续,请将其包装在Task.Run
中。这将使代码的意图更加清晰,并使代码对未来的框架更新更具弹性。此外,不要将SynchronizationContext.Current
设置为new SynchronizationContext()
,因为对该方案的处理不一致。
SynchronizationContext
总是简单地在帖子上调用ThreadPool.QueueUserWorkItem
- 这解释了为什么你总是在测试 #2 中看到不同的线程。
在测试 #1 中,您使用的是更智能的TaskScheduler
。await
应该在同一线程上继续(或"停留在当前线程上")。 在控制台应用中,无法像在基于消息队列的 UI 框架中那样"计划"返回到主线程。控制台应用程序中的await
必须阻塞主线程,直到工作完成(使主线程无事可做),才能在同一线程上继续。 如果调度程序知道这一点,那么它最好在同一线程上同步运行代码,因为它将具有相同的结果,而不必创建另一个线程并冒上下文切换的风险。
更多信息可以在这里找到:http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx
更新: 就ConfigureAwait
而言. 控制台应用程序无法"封送"回主线程,因此,据推测,ConfigureAwait(false)
在控制台应用程序中没有任何意义。
另请参阅:http://msdn.microsoft.com/en-us/magazine/jj991977.aspx