博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Task机制

Posted on 2023-05-27 19:25  qianyz  阅读(21)  评论(0编辑  收藏  举报

来源:

[.NET]Thread与Task的区别 - 大杂草 - 博客园 (cnblogs.com)

(12条消息) C# 多线程七 任务Task的简单理解与运用一_c# task_一梭键盘任平生的博客-CSDN博客

以下几张图片能够清晰看出task运行大概原理

Thread

 

 

Task

 

 

 

 

 

ThreadPool的运行原理

 

 

Task的运行原理:(包含task中的task)

 

 

Task与ThreadPool什么关系呢?简单来说,Task是基于ThreadPool实现的,当然被标记为LongRunning的Task(单独创建线程实现)除外。Task被创建后,通过TaskScheduler执行工作项的分配。TaskScheduler会把工作项存储到两类队列中: 全局队列与本地队列。全局队列被设计为FIFO(先进先出)的队列。本地队列存储在线程中,被设计为LIFO.(先进后出或者后进先出)

    当主程序创建了一个Task后,由于创建这个Task的线程不是线程池中的线程,则TaskScheduler 会把该Task放入全局队列中。

    如果这个Task是由线程池中的线程创建,并且未设置TaskCreationOptions.PreferFairness标记(默认情况下未设置),TaskScheduler 会把该Task放入到该线程的本地队列中。如果设置了TaskCreationOptions.PreferFairness标记,则放入全局队列。

    官方的解释是: 最高级任务(即不在其他任务的上下文中创建的任务)与任何其他工作项一样放在全局队列上。 但是,嵌套任务或子任务(在其他任务的上下文中创建)的处理方式大不相同。 子任务或嵌套任务放置在特定于执行父任务的线程的本地队列上。 父任务可能是最高级任务,也可能是其他任务的子任务。

    那么任务放入到两类队列中后,是如何被执行的呢?

    当线程池中的线程准备好执行更多工作时,首先查看本地队列。 如果工作项在此处等待,直接通过LIFO的模式获取执行。 如果没有,则向全局队列以FIFO的模式获取工作项。如果全局队列也没有工作项,则查看其他线程的本地队列是否有可执行工作项,如果存在可执行工作项,则以FIFO的模式出队执行。

 

 

=================================

飞蛾 - 新的和改进的 CLR 4 线程池引擎 (danielmoth.com)

考虑 CLR 线程池及其使用方式的一种方法是(例如,当您调用 ThreadPool.QueueUserWorkItem 时)是想象一个全局队列,其中工作项(本质上是委托)在全局队列上排队,多个线程按先进先出顺序挑选它们。FIFO订单不是记录或保证的东西,但我个人的猜测是太多的应用程序依赖于它,所以我不认为它很快就会改变。

左侧的图像显示了主程序线程在创建工作项时;第二个图像显示了代码将 3 个工作项排队后的全局线程池队列;第三张图像显示了线程池中的 2 个线程,这些线程已获取 2 个工作项并执行它们。如果在这些工作项的上下文中(即从委托中的执行代码中)为 CLR 线程池创建了更多工作项,则它们最终会进入全局队列(参见右图),并且生活将继续。

从 System.Threading.Tasks 角度
看 CLR Thread Pool v4 在 CLR 4 中,线程池引擎对其进行了一些改进(在 CLR 的每个版本中都进行了积极的调整),这些改进的一部分是使用新的 System.Threading.Tasks.Task 类型时可以实现的一些性能提升。我将在另一篇文章中展示一个代码示例,但您可以将创建和启动任务(向其传递委托)视为在线程池上调用 QueueUserWorkItem 等效。通过基于任务的 API 使用时可视化 CLR 线程池的一种方法是,除了单个全局队列之外,线程池中的每个线程都有自己的本地队列:

与正常的线程池使用一样,主程序线程可能会创建将在全局队列(例如 Task1 和 Task2)上排队的任务,线程通常以 FIFO 方式抓取这些任务。分歧在于,在执行任务(例如 Task2)的上下文中创建的任何新任务(例如 Task3)最终都会出现在该线程池线程的本地队列中。

为什么选择本地队列
随着众核机器时代的到来和开发人员利用并行性,线程池中的线程数可能会增加:至少等于计算绑定操作的内核数,并且可能更多是由于 IO 绑定操作或阻塞调用导致 CPU 停滞而导致额外线程的注入。底线:更多内核 = 更多线程。

随着更多线程争用工作项,忍受所有线程都尝试安全访问的单个队列的争用问题并不是最佳选择。这将通过细粒度并行性的目标来放大,其中每个工作项都相当快地完成,因此到全局队列的行程将很频繁。

正是出于这个原因,我们为每个线程引入了一个本地队列,其中其他任务将排队(没有争用),然后由同一线程检索(同样没有争用)。

后进先出
因此,从图片进一步来看,让我们假设 Task2 还创建了另外两个任务,例如 Task4 和 Task5。

任务按预期最终出现在本地队列中,但线程在完成其当前任务(即 Task2)时选择执行哪个任务?这最初令人惊讶的答案是,它可能是Task5,这是最后一个排队的 - 换句话说,LIFO算法可用于本地队列。

这是一件好事的原因是地方性。在大多数情况下,队列中最后创建的任务所需的数据在缓存中仍然是热的,因此将其拉下并执行它是有意义的。显然,这意味着没有订购承诺,但为了更好的性能,放弃了一定程度的公平。

工作窃取
如果唯一的增强功能是引入后进先出本地队列,那么性能将大大提高,但您应该担心。您应该关注当线程池中的另一个线程(可能在另一个内核上执行)完成其工作时会发生什么。幸运的是,您不必:

另一个工作线程完成 Task1,然后转到其本地队列并发现它是空的;然后,它转到全局队列并发现它是空的。我们不希望它闲置在那里,所以发生了一件美好的事情:偷工作。线程进入另一个线程的本地队列并“窃取”任务并执行它!这样,我们就可以保持所有内核的繁忙状态,这有助于实现细粒度并行负载均衡目标。在上图中,请注意“窃取”以FIFO方式发生,出于局部原因,这同样是好的(其数据在缓存中将是冷的)。此外,在许多分而治之的场景中,之前生成的任务本身可能会生成更多的工作(例如 Task6),这些工作现在最终会进入另一个线程的队列,从而减少频繁的窃取。
吹毛求疵:再往上,我提到了“没有争议”;显然,偷窃工作是该规则的例外。