Work-stealing 算法及 C++ 实现介绍

server/2024/12/14 8:09:30/

在这里插入图片描述

一、Work-stealing 算法历史

Work-stealing 是一种高效的任务调度算法,通常用于并行计算框架中,例如 Intel TBB 和 Java Fork/Join 框架。它的核心思想是每个线程都有一个任务队列,当某个线程完成自身任务时,它会尝试从其他线程的队列中窃取任务继续执行。

Working-Stealing算法最初是由Charles E. Leiserson、 Blaise Barney、 Daniel Cociorva 以及 Andrew Begel在2009年的论文“The implementation of work stealing in Intel Cilk”中提出的.

  • 提出场景:随着计算机硬件技术的发展,多核处理器逐渐普及,并行计算成为提高计算性能的重要手段。在并行编程中,如何有效地调度任务以充分利用多核处理器的资源,同时减少任务之间的等待时间和处理器的空闲时间,成为一个重要的研究课题。在此背景下,Working-Stealing算法应运而生,为解决并行任务调度问题提供了一种有效的策略.
  • 最初目的
    • 实现高效的负载平衡:在并行计算中,不同任务的执行时间可能存在差异,导致某些处理器可能在完成自己的任务后处于空闲状态,而其他处理器仍在忙碌。Working-Stealing算法通过允许空闲处理器从忙碌处理器的任务队列中窃取任务,可以自动地将工作负载均匀地分配到各个处理器上,充分利用所有处理器的计算资源,提高整体的计算效率.
    • 减少同步开销:与传统的使用全局任务队列或集中式调度的方法相比,Working-Stealing算法为每个处理器或线程分配了独立的任务队列。这种分散式的任务管理方式减少了多个处理器同时访问共享资源时的竞争和同步开销,提高了系统的并行性和可扩展性.
    • 提高数据局部性:在窃取任务时,处理器通常会从其他处理器的任务队列尾部窃取任务。由于任务队列中的任务往往具有一定的相关性或数据依赖性,这种窃取方式有利于提高数据的局部性,即窃取到的任务更有可能操作与当前处理器正在处理的数据相近的数据,从而减少了数据访问的延迟,进一步提高了性能.
    • 适应动态变化的任务执行时间:在实际的并行计算中,任务的执行时间可能会因为各种因素而发生变化,例如数据的分布、缓存的命中率等。Working-Stealing算法能够根据任务的实际执行情况动态地调整任务的分配,及时地将任务从执行时间较长的处理器转移到空闲处理器上,从而更好地适应这种动态变化,保持系统的高性能运行.

二、Work-stealing 算法定义

  1. 工作窃取(Working - Stealing)算法的定义

    • 基本概念:工作窃取是一种用于动态负载平衡的任务调度算法。在并行计算环境中,有多个线程(或处理器)可用于执行任务。每个线程都有自己的任务队列,当一个线程完成自己队列中的所有任务后,它会从其他线程的任务队列尾部“窃取”任务来执行。
    • 任务分配方式:例如,假设有线程A、B和C,它们都有各自的任务队列。开始时,每个线程从自己的队列头部获取任务并执行。如果线程A提前完成了自己队列中的所有任务,而线程B和C还有任务未完成,那么线程A会尝试从线程B或C的任务队列尾部“拿走”一个任务来继续执行。这种从其他线程队列尾部窃取任务的方式是为了尽量减少对被窃取线程的干扰,因为一般情况下,线程会从队列头部获取任务执行,这样就可以避免与正在被窃取线程的任务执行产生冲突。
  2. 成为众多并行计算库核心算法的原因

    • 动态负载平衡优势
      • 自动适应负载变化:在并行计算中,任务的执行时间可能因各种因素而不同。有些任务可能很快完成,而有些任务可能由于数据依赖、计算复杂性等原因需要更长时间。工作窃取算法能够自动适应这种负载的动态变化。例如,在一个并行处理图像像素的任务中,部分像素区域可能因为颜色单一、处理简单而快速完成处理,而包含复杂纹理或对象的像素区域处理时间较长。采用工作窃取算法,处理简单区域的线程在完成自己的任务后,可以帮助处理复杂区域的线程,从而平衡各个线程的负载,提高整体的计算效率。
      • 减少空闲时间:它有效地减少了线程的空闲时间。在传统的任务分配方式中,如果任务分配不均匀,可能会出现部分线程提前完成任务而闲置,而其他线程仍在忙碌的情况。工作窃取算法通过让空闲线程主动获取任务,使得所有线程都能尽可能地保持忙碌状态,充分利用了计算资源。比如在一个基于多核处理器的并行计算服务器上,工作窃取算法可以确保各个核心都能高效地参与计算,避免核心资源的浪费。
    • 可扩展性良好
      • 适应多核处理器和大规模集群:随着计算机硬件的发展,多核处理器和大规模计算集群越来越普遍。工作窃取算法能够很好地适应这种硬件环境的扩展。在多核处理器中,增加核心数量时,算法可以自动将任务分配到新的核心上,而不需要对任务进行大规模的重新分配。同样,在大规模集群计算中,新加入的计算节点也可以通过工作窃取机制参与到任务执行中。例如,在一个超级计算机集群处理大规模气象模拟数据的场景中,当新的计算节点加入集群时,工作窃取算法可以让这些节点自动分担其他节点的任务,实现高效的并行计算。
    • 易于实现和集成
      • 对现有并行编程模型的友好性:工作窃取算法相对容易理解和实现,它可以作为一个独立的调度模块集成到现有的并行编程框架中。许多并行编程语言和库,如Cilk等,都采用了工作窃取算法来实现高效的任务调度。对于开发者来说,他们可以在不改变原有并行算法的基本逻辑的情况下,利用工作窃取算法来提高程序的性能。例如,在一个基于任务并行的科学计算程序中,开发者只需要将任务分配和调度部分替换为工作窃取机制,就可以在一定程度上提升程序在多核环境下的运行效率。

三、C++ 实现介绍

c++20实现如下:

实现代码

#include <iostream>
#include <vector>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <random>
#include <functional>
#include <optional>class WorkStealingQueue {
private:std::deque<std::function<void()>> tasks;mutable std::mutex mtx;public:// 推送任务到队列尾部void pushTask(std::function<void()> task) {std::lock_guard<std::mutex> lock(mtx);tasks.push_back(std::move(task));}// 弹出队列尾部的任务(本线程优先)std::optional<std::function<void()>> popTask() {std::lock_guard<std::mutex> lock(mtx);if (tasks.empty()) return std::nullopt;auto task = std::move(tasks.back());tasks.pop_back();return task;}// 从队列头部窃取任务(其他线程)std::optional<std::function<void()>> stealTask() {std::lock_guard<std::mutex> lock(mtx);if (tasks.empty()) return std::nullopt;auto task = std::move(tasks.front());tasks.pop_front();return task;}// 检查队列是否为空bool isEmpty() const {std::lock_guard<std::mutex> lock(mtx);return tasks.empty();}
};class WorkStealingScheduler {
private:std::vector<WorkStealingQueue> queues;std::vector<std::thread> workers;std::atomic<bool> stop{false};unsigned int threadCount;public:WorkStealingScheduler(unsigned int threadCount): queues(threadCount), threadCount(threadCount) {}~WorkStealingScheduler() {stop = true;for (auto& worker : workers) {if (worker.joinable()) worker.join();}}void start() {for (unsigned int i = 0; i < threadCount; ++i) {workers.emplace_back([this, i]() { workerThread(i); });}}void submitTask(unsigned int threadId, std::function<void()> task) {queues[threadId].pushTask(std::move(task));}private:void workerThread(unsigned int id) {std::mt19937 rng(id); // 用于随机选择其他线程队列std::uniform_int_distribution<unsigned int> dist(0, threadCount - 1);while (!stop) {// 尝试从自己的队列获取任务auto task = queues[id].popTask();if (!task) {// 如果没有任务,从其他线程的队列中窃取for (unsigned int attempt = 0; attempt < threadCount; ++attempt) {unsigned int victimId = dist(rng);if (victimId!= id) {task = queues[victimId].stealTask();if (task) break;}}}// 执行任务if (task) {(*task)();} else {// 如果没有任务可执行,线程暂时休眠一会儿std::this_thread::yield();}}}
};int main() {constexpr unsigned int numThreads = 4;WorkStealingScheduler scheduler(numThreads);scheduler.start();// 提交任务for (unsigned int i = 0; i < 20; ++i) {scheduler.submitTask(i % numThreads, [i]() {std::cout << "Task " << i << " executed by thread " << std::this_thread::get_id() << std::endl;});}// 等待所有任务完成std::this_thread::sleep_for(std::chrono::seconds(1));return 0;
}

代码说明

1. WorkStealingQueue

  • 每个线程都有一个 WorkStealingQueue,用于存储自己的任务。
  • 支持本线程的任务弹出 (popTask) 和其他线程的任务窃取 (stealTask)。

2. WorkStealingScheduler

  • 包含多个 WorkStealingQueue,每个线程对应一个队列。
  • 提供 submitTask 接口向特定线程的队列添加任务。
  • 每个线程会从自己的队列中取任务执行,如果没有任务会随机尝试从其他队列窃取任务。

3. 随机窃取

  • 使用 std::mt19937 生成随机数,选择其他线程的队列窃取任务。

4. 任务执行

  • 每个任务是一个 std::function<void()>,它可以封装任意的可调用对象。

5. 停止逻辑

  • 调用 ~WorkStealingScheduler() 会停止所有线程。

示例输出

运行程序后会看到类似的输出:

Task 0 executed by thread 140390394259200
Task 1 executed by thread 140390385866496
Task 2 executed by thread 140390377473792
Task 3 executed by thread 140390394259200
Task 4 executed by thread 140390385866496
...

任务由多个线程并行执行,如果某个线程空闲,它会从其他线程的队列中窃取任务。

改进方向

1. 负载均衡

增加任务优先级,改进任务窃取逻辑。

2. 更高效的数据结构

使用无锁队列替代互斥锁保护的双端队列。

3. 任务追踪

添加任务完成信号,确保所有任务执行完毕后主线程退出。

通过这些改进,可以进一步提升调度器的性能和功能。


http://www.ppmy.cn/server/150041.html

相关文章

内存卡格式化后的数据恢复全攻略

一、内存卡格式化简述 内存卡&#xff0c;作为现代电子设备中不可或缺的存储媒介&#xff0c;广泛应用于手机、相机、行车记录仪等各类设备中。然而&#xff0c;在使用过程中&#xff0c;我们可能会遇到内存卡需要格式化的情况。格式化是一种将内存卡上的所有数据和文件系统清…

Python跳动的爱心

系列文章 序号直达链接表白系列1Python制作一个无法拒绝的表白界面2Python满屏飘字表白代码3Python无限弹窗满屏表白代码4Python李峋同款可写字版跳动的爱心5Python流星雨代码6Python漂浮爱心代码7Python爱心光波代码8Python普通的玫瑰花代码9Python炫酷的玫瑰花代码10Python多…

数据库原理期末知识点复习

第一章 概论 一、基本概念 数据&#xff08;Data&#xff09;是数据库中存储的基本对象&#xff0c;描述事物的符号记录。例如学生的学号、姓名等信息都是数据。数据库&#xff08;Database&#xff0c;DB&#xff09;长期存储在计算机内、有组织的、可共享的大量数据的集合。…

Docker学习路线图

‌‌Docker‌是一种开源的容器化技术&#xff0c;它通过提供一个轻量级的虚拟化环境&#xff0c;使得开发者可以打包、分发和运行应用程序及其依赖环境。Docker将应用程序及其所有依赖项打包成一个“容器”&#xff0c;这个容器可以在任何支持Docker的系统上运行&#xff0c;而…

Motrix WebExtension 使用教程

Motrix WebExtension 使用教程 项目地址:https://gitcode.com/gh_mirrors/mo/motrix-webextension 项目介绍 Motrix WebExtension 是一个浏览器扩展,用于与 Motrix 下载管理器集成。该扩展允许用户通过 Motrix 下载管理器自动下载文件,而不是使用浏览器的原生下载管理器。…

linux学习笔记01 基础命令

目录 创建 touch 创建文件 &#xff08;创建但是不打开&#xff09; vi / vim 创建文件 (创建一个文件并打开) mkdir 创建文件夹 切换目录 cd 查看 pwd 查看当前目录完整路径 ls 查看目录信息 dir 查看目录信息 ll 表示查看目标目录下的信息 ls -a 查看当前目录下的…

lspci简介

lspci命令用于列出系统中所有pci设备信息,其输出信息包括设备的bdf地址(总线号、设备号和功能号)、设备类型、厂商信息等。 lspci命令的基本用法: lspci:列出所有pci设备的详细信息 参数: -v:显示详细信息,包括驱动程序、总线和端口等信息 -t:以属性结构显…

漫谈网络安全策略

1.引言 随着计算机网络的不断发展&#xff0c;全球信息化已成为人类发展的大趋势。但由于计算机网络具有联结形式多样性、终端分布不均匀性和网络的开放性、互连性等特征&#xff0c;致使网络易受黑客、怪客、恶意软件和其他不轨的攻击&#xff0c;所以网上信息的安全和保密是一…