根据 TaskCreationOptions 的不同,出现了三个分支
进入全局队列的任务能够公平地被各个线程池中的线程领取执行,也是就是 prefer fairness
这个词组的字面意思了。
下图中 Task666 先进入全局队列,随后被 Thread1 领走。Thread3 通过 WorkStealing 机制窃取了 Thread2 中的 Task2。
在使用Task.Factory.StartNew或者Task.Factory.FromAsync方法创建任务时,一些重载方法允许提供TaskCreationOptions来向调度器提示任务的调度方案。这里简要介绍了AttachedToParent、DenyChildAttach、HideScheduler、LongRunning、PreferFairness五种选项的具体行为。
在一个Task中创建另一个Task时,爸爸Task通常不会等待儿子Task结束。
例如:
using System;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
public static void Main()
{
var parent = Task.Factory.StartNew(() => {
Console.WriteLine("Outer task executing.");
var child = Task.Factory.StartNew(() => {
Console.WriteLine("Nested task starting.");
Thread.SpinWait(500000);
Console.WriteLine("Nested task completing.");
});
});
parent.Wait();
Console.WriteLine("Outer has completed.");
}
}
// The example produces output like the following:
// Outer task executing.
// Nested task starting.
// Outer has completed.
// Nested task completing.
相应地,使用TaskCreationOptions.AttachedToParent创建的儿子Task则带有以下3个特点(这3个特点也是默认情况下创建的儿子Task不具备的):
例如,上面的代码使用TaskCreationOptions.AttachedToParent,则会得到以下的输出:
using System;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
public static void Main()
{
var parent = Task.Factory.StartNew(() => {
Console.WriteLine("Parent task executing.");
var child = Task.Factory.StartNew(() => {
Console.WriteLine("Attached child starting.");
Thread.SpinWait(5000000);
Console.WriteLine("Attached child completing.");
}, TaskCreationOptions.AttachedToParent);
});
parent.Wait();
Console.WriteLine("Parent has completed.");
}
}
// The example displays the following output:
// Parent task executing.
// Attached child starting.
// Attached child completing.
// Parent has completed.
如果你不希望一个Task的启动的儿子们Attach到它自己身上,则可以在启动爸爸Task时为它指定TaskCreationOptions.DenyChildAttach。当通过DenyChildAttach启动的爸爸Task试图指定AttachedToParent来启动儿子Task时,AttachedToParent将会失效。
当指定TaskCreationOptions.HideScheduler时,创建Task里再创建的儿子Task将使用默认的TaskScheduler,而不是当前的TaskScheduler。这相当于在创建Task时隐藏了自己当前的TaskScheduler。对于本身就是在默认的TaskScheduler里创建的Task,这个选项似乎没什么用。
using System.Reflection;
Task tasktest = new(() => { Console.WriteLine($"test Task TaskScheduler is {TaskScheduler.Current}"); });
Task taskParent = new(() => {
//这边不能使用Task.Run 因为它已经配置配置好了,用的是线程池任务调度器。
Task subtask=new(() =>
{
//这边直接使用了 父任务的任务调度器
Console.WriteLine($"sub Task TaskScheduler is {TaskScheduler.Current}");
});
subtask.Start();//这边使用的是当前线程,所以继承了父任务的 任务调度器
Console.WriteLine($"main Task TaskScheduler is {TaskScheduler.Current}");
Task subtask2 = new(() =>
{
//因为隐藏父任务的任务调度器,所以采用了默认的线程池调度器
Console.WriteLine($"sub Task2 TaskScheduler is {TaskScheduler.Current}");
},TaskCreationOptions.HideScheduler);// 隐藏父类的任务调度器
subtask2.Start();
Console.WriteLine($"main Task TaskScheduler is {TaskScheduler.Current}");
});
tasktest.Start();
taskParent.Start(new PerThreadTaskScheduler());//自定义的任务调度器
taskParent.Wait();
Console.WriteLine("all complete");
Console.Read();
/* 输出:
test Task TaskScheduler is System.Threading.Tasks.ThreadPoolTaskScheduler
main Task TaskScheduler is PerThreadTaskScheduler
sub Task TaskScheduler is PerThreadTaskScheduler
main Task TaskScheduler is PerThreadTaskScheduler
sub Task2 TaskScheduler is System.Threading.Tasks.ThreadPoolTaskScheduler
all complete
*/
自定义的任务调度器
public class PerThreadTaskScheduler : TaskScheduler
{
public string Name => "PerThreadTaskScheduler ";
protected override IEnumerable
{
return null;
}
protected override void QueueTask(Task task)
{
var thread = new Thread(() =>
{
TryExecuteTask(task);
});
thread.Start();
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
throw new NotImplementedException();
}
}
C#启动的Task都会通过TaskScheduler来安排执行。根据官方文档的描述:
The default scheduler for the Task Parallel Library and PLINQ uses the .NET Framework thread pool, which is represented by the ThreadPool class, to queue and execute work. The thread pool uses the information that is provided by the Task type to efficiently support the fine-grained parallelism (short-lived units of work) that parallel tasks and queries often represent.
而默认的TaskScheduler采用的是.NET线程池ThreadPool,它主要面向的是细粒度的小任务,其执行时间通常在毫秒级。线程池中的线程数与处理器的内核数有关,如果线程池中没有空闲的线程,那么后续的Task将会被阻塞。因此,如果事先知道一个Task的执行需要较长的时间,就需要使用TaskCreationOptions.LongRunning枚举指明。使用TaskCreationOptions.LongRunning创建的任务将会脱离线程池启动一个单独的线程来执行。
任务并行库实现良好性能的方法之一是通过"工作窃取"。.NET 4 线程池支持工作窃取,以便通过任务并行库及其默认计划程序进行访问。这表现为线程池中的每个线程都有自己的工作队列;当该线程创建任务时,默认情况下,这些任务将排队到线程的本地队列中,而不是排队到对 ThreadPool.QueueUserWorkItem 的调用通常面向的全局队列中。当线程搜索要执行的工作时,它会从其本地队列开始,该操作由于改进了缓存局部性,最小化了争用等,从而实现了一些额外的效率。但是,这种逻辑也会影响公平性。
典型的线程池将具有单个队列,用于维护要执行的所有工作。当池中的线程准备好处理另一个工作项时,它们将从队列的头部取消排队工作,当新工作到达池中执行时,它将排队到队列的尾部。这为工作项之间提供了一定程度的公平性,因为首先到达的工作项更有可能被选中并首先开始执行。
偷工作扰乱了这种公平。池外部的线程可能正在排队工作,但如果池中的线程也在生成工作,则池生成的工作将优先于其他工作项,具体取决于池中线程(这些线程首先开始使用其本地队列搜索工作, 仅继续进入全局队列,然后继续到其他线程的队列(如果本地没有工作可用)。这种行为通常是预期的,甚至是期望的,因为如果正在执行的工作项正在生成更多工作,则生成的工作通常被视为正在处理的整体操作的一部分,因此它比其他不相关的工作更可取是有道理的。例如,想象一个快速排序操作,其中每个递归排序调用都可能导致几个进一步的递归调用;这些调用(在并行实现中可能是单个任务)是全系列排序操作的一部分。
不过,在某些情况下,这种默认行为是不合适的,其中应该在池中的线程生成的特定工作项和其他线程生成的工作项之间保持公平性。对于长链的延续,通常就是这种情况,其中生成的工作不被视为当前工作的一部分,而是当前工作的后续工作。在这些情况下,您可能希望以公平的方式将后续工作与系统中的其他工作放在一起。这就是TaskCreationOptions.PreferFairness可以证明有用的地方。
将 Task 调度到默认调度程序时,调度程序将查看任务从中排队的当前线程是否是具有自己的本地队列的 ThreadPool 线程。如果不是,则工作项将排队到全局队列。如果是,计划程序还将检查任务的 TaskCreationOptions 值是否包含"首选公平性"标志,默认情况下该标志未打开。如果设置了该标志,即使线程确实有自己的本地队列,调度程序仍将 Task 排队到全局队列,而不是本地队列。通过这种方式,该任务将与全局排队的所有其他工作项一起被公平地考虑。
刚才描述的是默认计划程序中优先公平标志的当前实现。实现当然可以更改,但不会更改的是标志的目的:通过指定 PreferFairness,您可以告诉系统不应仅仅因为此任务来自本地队列而对其进行优先级排序。您是在告诉系统,您希望系统尽最大努力确保此任务以先到先得的方式进行优先级排序。
另一件需要注意的事情是,Task本身对这面旗帜一无所知。它只是一个标志,设置为任务上的一个选项。调度程序决定了它想要如何处理这个特定的选项,就像TaskCreationOptions.LongRunning一样。默认调度程序按上述方式处理它,但另一个调度程序(例如您编写的调度程序)可以根据需要使用此标志,包括忽略它。因此,命名"首选"而不是像"保证"这样更严格的东西。
转载自:TaskCreationOptions.PreferFairness - .NET Parallel Programming (microsoft.com)
手机扫一扫
移动阅读更方便
你可能感兴趣的文章