默认同步上下文与默认任务计划程序



这会有点长,所以请耐心等待。

我认为默认任务调度程序(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视为默认计划程序,而其他类型将nullnew SynchronizationContext()视为默认计划程序。

当你await一个不完整的Task时,TaskAwaiter默认捕获当前SynchronizationContext- 除非它是null的(或其GetType返回typeof(SynchronizationContext)),在这种情况下,TaskAwaiter会捕获当前TaskScheduler。此行为主要记录在案(GetType子句不是 AFAIK)。但是,请注意,这描述的是TaskAwaiter的行为,而不是TaskScheduler.DefaultTaskFactory.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_synchronizationContextnew SynchronizationContext()创建的实例;但是,该线程的SynchronizationContext.Currentnull
  • SetResult尝试在当前任务计划程序上同步执行await tcs.Task延续;但是,它不能,因为SynchronizationContextTaskScheduler看到线程 10 在需要new SynchronizationContext()时具有nullSynchronizationContext.Current。因此,它异步调度延续(在线程 11 上)。
  • 类似的情况发生在StartNew任务结束时;在这种情况下,我相信await task继续在同一线程上是巧合的。

最后,我必须强调,依赖未记录的实施细节是不明智的。如果要让async方法在线程池线程上继续,请将其包装在Task.Run中。这将使代码的意图更加清晰,并使代码对未来的框架更新更具弹性。此外,不要将SynchronizationContext.Current设置为new SynchronizationContext(),因为对该方案的处理不一致。

SynchronizationContext总是简单地在帖子上调用ThreadPool.QueueUserWorkItem- 这解释了为什么你总是在测试 #2 中看到不同的线程。

在测试 #1 中,您使用的是更智能的TaskSchedulerawait应该在同一线程上继续(或"停留在当前线程上")。 在控制台应用中,无法像在基于消息队列的 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

相关内容

  • 没有找到相关文章

最新更新