任务计划程序
任务计划程序由 System.Threading.Tasks.TaskScheduler 类表示。 任务计划程序确保任务的工作得以最终执行。 默认任务计划程序基于 .NET Framework 4 ThreadPool,后者提供了用于负载平衡的工作窃取、用于实现最大吞吐量的线程注入/撤消,以及总体出色的性能。 它应足以应对大多数情况。 但是,如果需要特殊功能,您可以创建自定义计划程序,并为特定任务或查询启用该计划程序。 有关如何创建和使用自定义任务计划程序的更多信息,请参见如何:创建限制并发程度的任务计划程序。 有关自定义计划程序的其他示例,请参见 MSDN 代码库网站上的 Parallel Extensions Samples(并行扩展示例)。
默认任务计划程序和 ThreadPool
任务并行库和 PLINQ 的默认计划程序使用 .NET Framework ThreadPool 对工作排队和执行工作。 在 .NET Framework 4 中,ThreadPool 使用 System.Threading.Tasks.Task 类型提供的信息来有效地支持并行任务和查询通常表示的细化并行(生存期较短的工作单元)。
ThreadPool 全局队列与本地队列
就像在 .NET Framework 的早期版本中一样,ThreadPool 为每个应用程序域中的线程维护一个全局 FIFO(先进先出)工作队列。 每当程序调用 QueueUserWorkItem(或 UnsafeQueueUserWorkItem)时,工作将放入此共享队列,并最终离队进入下一个可用的线程。 在 .NET Framework 4 中,此队列已经过改进,可以使用类似于 ConcurrentQueue 类的无锁算法。通过使用此无锁实现,减少了 ThreadPool 在对工作项进行排队和离队操作时花费的时间。 使用 ThreadPool 的所有程序都可获得此性能优势。
就像任何其他工作项一样,顶级任务(即不是在另一个任务的上下文中创建的任务)将放入全局队列。 而在另一个任务的上下文中创建的嵌套任务或子任务则以完全不同的方式处理。 子任务或嵌套任务将放入一个本地队列,该队列特定于父任务在其上执行的线程。 父任务可能是顶级任务,也可能是另一个任务的子级。 当此线程准备好执行更多工作时,它将首先在本地队列中查找。 如果工作项正等在那里,则很快就会被访问。 本地队列将按后进先出顺序 (LIFO) 访问,以便保留缓存位置并减少争用。 有关子任务和嵌套任务的更多信息,请参见嵌套任务和子任务。
下面的示例演示一些安排在全局队列上的任务,以及安排在本地队列上的其他任务。
Sub QueueTasks()
' TaskA is a top level task.
Dim taskA = Task.Factory.StartNew(Sub()
Console.WriteLine("I was enqueued on the thread pool's global queue.")
' TaskB is a nested task and TaskC is a child task. Both go to local queue.
Dim taskB = New Task(Sub() Console.WriteLine("I was enqueued on the local queue."))
Dim taskC = New Task(Sub() Console.WriteLine("I was enqueued on the local queue, too."),
TaskCreationOptions.AttachedToParent)
taskB.Start()
taskC.Start()
End Sub)
End Sub
void QueueTasks()
{
// TaskA is a top level task.
Task taskA = Task.Factory.StartNew( () =>
{
Console.WriteLine("I was enqueued on the thread pool's global queue.");
// TaskB is a nested task and TaskC is a child task. Both go to local queue.
Task taskB = new Task( ()=> Console.WriteLine("I was enqueued on the local queue."));
Task taskC = new Task(() => Console.WriteLine("I was enqueued on the local queue, too."),
TaskCreationOptions.AttachedToParent);
taskB.Start();
taskC.Start();
});
}
使用本地队列不仅可以减轻全局队列上的压力,而且可以利用数据位置。 本地队列中的工作项经常引用内存中的物理位置互相接近的数据结构。 在这些情况下,数据在第一个任务运行后已位于缓存中,并且可快速访问。 并行 LINQ (PLINQ) 和 Parallel 类都广泛使用嵌套任务和子任务,并通过使用本地工作队列实现了明显的加速。
工作窃取
.NET Framework 4 ThreadPool 还提供了工作窃取算法,可帮助确保当其他线程在其队列仍然有工作时,没有线程处于空闲状态。 当线程池线程准备好执行更多工作时,它将首先在其本地队列的开始部分查找,然后依次在全局队列和其他线程的本地队列中查找。 如果它在另一个线程的本地队列中找到工作项,将会首先应用试探法来确保能够有效地运行该工作。 如果能够有效运行工作,它将(按 LIFO 顺序)使队列末尾的工作项离队。 这样可以减少每个本地队列上的争用并保留数据位置。与以前的版本相比,此体系结构可帮助 .NET Framework 4 ThreadPool 更有效地平衡工作的负载。
长时间运行的任务
您可能需要明确防止将任务放入本地队列。 例如,您可能知道某个特定工作项将运行相对很长的时间,并有可能阻塞本地队列中的所有其他工作项。 在这种情况下,您可以指定 LongRunning 选项来提示计划程序该任务可能需要附加线程,以便它不会阻止其他线程或本地队列中的工作项的进展。 通过使用此选项,可以完全避免 ThreadPool(包括全局队列和本地队列)。
任务内联
在某些情况下,当等待任务时,该任务可在正在执行等待操作的线程上以同步方式执行。 这样可提高性能,因为它利用了将以其他方式阻止的现有线程,因此不需要附加线程。 为了防止由于重新进入而导致的错误,只有在相关线程的本地队列中找到了等待目标的情况下,才会进行任务内联。
指定同步上下文
可以使用 TaskScheduler.FromCurrentSynchronizationContext 方法来指定应将任务安排在特定线程上运行。 这一点在诸如 Windows Forms 和 Windows Presentation Foundation 等框架中非常有用,在这些框架中,通常只有在创建 UI 对象的相同线程上运行的代码才能访问用户界面对象。 有关更多信息,请参见如何:将工作安排在指定的同步上下文上。