C++ 中的工作窃取算法

2025 年 5 月 15 日 | 阅读 8 分钟

引言

在并行计算系统性能中,当存在多个处理器时,任务分配变得至关重要。工作窃取算法是一种适用于此环境的有效负载平衡方法。工作窃取方法允许已完成任务的线程“窃取”具有未完成任务的线程,从而保持系统平衡并减少空闲处理器。它现已广泛应用于支持并行性的框架中,例如 Intel 的 Threading Building Blocks (TBB) 和 Java 中的 ForkJoinPool。

在本文中,我们将首先概述工作窃取算法的概念,然后提供一个实现它的 C++ 代码。此外,还将讨论工作窃取算法的适当用法和限制。

问题陈述

在多线程模型中,一些线程会比其他线程更早完成它们的部分,因此一些资源将保持未使用状态。这就产生了如何在线程之间有效地平衡负载的问题,以便在其他线程有工作时,没有线程是空闲的。传统的任务分配方法通常使用集中式队列。这种方法也存在可伸缩性问题,因为可能会出现队列瓶颈。工作窃取算法的目标是创建一个分布式任务队列系统,空闲线程可以从其他线程的队列中“窃取”任务,以保持平衡并提高整体效率。

工作窃取算法概述

工作窃取算法通过允许每个线程拥有自己的双端队列(deque)来存储任务来操作。线程通常遵循以下规则:

  1. 每个线程主要在其自己的 deque 上操作,它以 LIFO(后进先出)顺序推送和弹出任务。
  2. 如果一个线程完成其任务且其 deque 为空,它会尝试从另一个线程的 deque 中“窃取”任务。
  3. 任务从 deque 的底部窃取,允许受害者线程将其最近的本地任务保留在顶部。
  4. 这种方法最大限度地减少了争用,因为大多数访问都是针对线程自己的 deque。

这种分散式方法允许更好的可伸缩性并避免单一的争用点,因为每个线程管理自己的任务队列,并且只有在任务用完时才与其他线程交互。

C++ 中的实现

以下是使用每个工作线程的 deque 实现工作窃取算法的一个简单 C++ 示例。这是一个用于说明目的的最小示例,缺少生产环境中所需的一些优化和错误处理。

程序 1

所需库

我们将使用 C++ 标准库的线程支持和容器进行实现。您可以使用 C++11 或更新的编译器编译此代码。

输出

Executing task 6 on thread 22639988467392
Executing task 2 on thread 22639988467392
Executing task 0 on thread 22639988467392
Executing task 4 on thread 22639988467392
Executing task 8 on thread 22639988467392
Executing task 1 on thread 22639988467392
Executing task 5 on thread 22639988467392
Executing task 9 on thread 22639988467392
Executing task 3 on thread 22639988467392
Executing task 7 on thread 22639986366144

代码解释

  • Worker 类:每个 worker 都有一个用于其任务的 deque、一个用于管理并发访问的互斥锁以及一个添加任务的方法 (push_task)。如果 worker 的 deque 为空,它会尝试从其他 worker 窃取任务。
  • 任务执行:每个 worker 在循环中运行,从其 deque 中弹出任务并执行它们。如果未找到任务,它会尝试从其他 worker 窃取任务。stop_flag 用于控制 worker 何时停止操作。
  • 任务窃取:当 worker 的 deque 中没有任务时,它会迭代其他 worker,尝试从其 deque 的底部窃取任务。如果没有可用的任务,它会短暂地放弃控制以减少 CPU 负载。
  • Main 函数主函数初始化 worker 并启动一组线程,每个线程运行一个 worker。它还在 worker 之间分配一些初始任务。短暂延迟后,所有 worker 都停止,并连接线程。

程序 2

输出

Task 1 executed on thread 140491566859968
Task 4 executed on thread 140491556370112   

说明

  1. 任务类
    • 它包含依赖关系,这些依赖关系包括依赖关系。
    • 原子计数器用于跟踪依赖关系,并确保此类依赖关系的安全和线程安全的递减。
  2. Worker 类
    • 它们维护一个私有的本地任务双端队列,并根据需要从中获取任务。
    • 任务在本地执行,或者在没有任务的情况下,甚至可以从其他工作线程中窃取。
  3. TaskScheduler 类
    • 监督分散在 worker 任务池中的 worker 线程网络。
    • 监控提交的任务并实时解决这些任务。
  4. 主函数
    • 展示了调度程序如何通过带有依赖关系和优先级的任务图来使用。
    • 任务在清除其依赖关系之前不会执行。

优点和权衡

工作窃取算法可以很好地补充线程,因为它允许工作负载平衡,从而增加 CPU 利用率和空闲时间。

  • 锁争用:即使算法允许大多数 deque 访问是本地的,但由于需要利用任务窃取概念,算法仍然允许大多数访问失效。
  • 任务开销:任务切换和窃取过多会导致一种任务开销,这反过来使得工作窃取对于持续时间非常短或没有计算要执行的任务来说并不实用。

结论

总之,工作窃取是一种处理多线程应用程序中动态工作负载的绝佳方法。它允许将部分工作分配到分布式队列中,并让空闲线程帮助工作量过大的线程。它使解决方案更具资源效率和高度可伸缩性。尽管该算法有其缺点,但它已经变得流行,并且更适用于需要多样化和可调整任务的领域,因此它成为并行计算领域中广泛使用的算法。