导读:本期聚焦于小伙伴创作的《C++如何实现高性能的多线程工作窃取队列来优化并发性能》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《C++如何实现高性能的多线程工作窃取队列来优化并发性能》有用,将其分享出去将是对创作者最好的鼓励。

多线程并发编程中,任务分配不均是常见性能瓶颈,工作窃取队列通过允许空闲线程窃取忙碌线程的待执行任务,实现负载均衡,提升整体执行效率。本文介绍C++实现高性能工作窃取队列的完整方案。

C++如何实现高性能的多线程工作窃取队列来优化并发性能

工作窃取队列的核心设计思路

工作窃取队列通常采用双端队列结构,每个工作线程维护自己的任务队列:线程自身从队列的一端(通常是尾部)推送和弹出任务,其他空闲线程从队列的另一端(头部)窃取任务,这样能减少不同线程操作同一端带来的竞争。

为了实现高性能,需要尽可能采用无锁设计,避免互斥锁带来的开销,同时要考虑缓存友好性,减少伪共享问题。核心操作包括三个:

  • 本地线程推送任务到队列尾部
  • 本地线程从队列尾部弹出任务执行
  • 其他线程从队列头部窃取任务

核心数据结构定义

首先定义任务类型和队列的基础结构,任务用函数对象表示,队列内部用环形数组存储任务,配合头尾索引实现双端操作。

#include <atomic>
#include <vector>
#include <functional>
#include <thread>
#include <cassert>

// 任务类型,无参数无返回值的函数对象
using Task = std::function<void()>;

class WorkStealingQueue {
private:
    // 环形数组存储任务,大小固定为2的幂次方便取模
    std::vector<Task> tasks;
    // 队列容量,必须是2的幂次
    size_t capacity;
    // 掩码,用于快速取模 capacity - 1
    size_t mask;
    // 头部索引,窃取线程操作的一端,使用原子变量保证可见性
    std::atomic<size_t> head;
    // 尾部索引,本地线程操作的一端,使用原子变量保证可见性
    std::atomic<size_t> tail;
    // 缓存行填充,避免head和tail伪共享
    char padding[64 - sizeof(std::atomic<size_t>)];

public:
    // 构造函数,初始化容量为2的幂次
    explicit WorkStealingQueue(size_t cap = 1024) : capacity(1) {
        // 确保容量是2的幂次
        while (capacity < cap) {
            capacity <<= 1;
        }
        mask = capacity - 1;
        tasks.resize(capacity);
        head.store(0, std::memory_order_relaxed);
        tail.store(0, std::memory_order_relaxed);
    }

    // 禁止拷贝构造和赋值
    WorkStealingQueue(const WorkStealingQueue&) = delete;
    WorkStealingQueue& operator=(const WorkStealingQueue&) = delete;
};

核心操作实现

本地推送任务

本地线程向队列尾部推送任务,只需要修改tail索引,不需要考虑其他线程的窃取操作,因为窃取操作只修改head索引,两者操作不同的原子变量,减少竞争。

    // 本地线程推送任务到队列尾部,返回是否成功
    bool push(Task task) {
        size_t t = tail.load(std::memory_order_relaxed);
        size_t h = head.load(std::memory_order_relaxed);
        // 队列已满,当前简单返回失败,实际可扩容
        if (t - h >= capacity) {
            return false;
        }
        tasks[t & mask] = std::move(task);
        // 使用release语义,保证任务写入对其他线程可见
        tail.store(t + 1, std::memory_order_release);
        return true;
    }

本地弹出任务

本地线程从队列尾部弹出任务,先读取tail和head,判断队列是否为空,不为空则修改tail索引,使用acquire语义保证读取到正确的任务。

    // 本地线程从队列尾部弹出任务,返回是否成功
    bool pop(Task& task) {
        size_t t = tail.load(std::memory_order_relaxed);
        size_t h = head.load(std::memory_order_relaxed);
        // 队列为空
        if (t == h) {
            return false;
        }
        // 先减tail,再读取任务,避免和窃取操作冲突
        t -= 1;
        tail.store(t, std::memory_order_relaxed);
        // 使用acquire语义,保证之前push的任务写入可见
        std::atomic_thread_fence(std::memory_order_acquire);
        task = std::move(tasks[t & mask]);
        // 再次检查是否有窃取操作发生,避免任务被重复取走
        h = head.load(std::memory_order_relaxed);
        if (t >= h) {
            return true;
        }
        // 窃取操作已经取走了所有任务,当前弹出无效
        tail.store(h, std::memory_order_relaxed);
        return false;
    }

窃取任务操作

其他线程从队列头部窃取任务,需要读取head和tail,判断队列是否有任务,然后尝试修改head索引,使用CAS操作保证原子性,避免多个窃取线程同时窃取同一个任务。

    // 其他线程从队列头部窃取任务,返回是否成功
    bool steal(Task& task) {
        size_t h = head.load(std::memory_order_relaxed);
        size_t t = tail.load(std::memory_order_acquire);
        // 队列为空
        if (h >= t) {
            return false;
        }
        task = tasks[h & mask];
        // 使用CAS操作修改head,避免多个窃取线程冲突
        if (head.compare_exchange_strong(h, h + 1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
            return true;
        }
        // CAS失败,说明其他线程已经窃取了任务,返回失败
        return false;
    }

完整使用示例

下面是一个简单的使用示例,创建多个工作线程,每个线程维护自己的工作窃取队列,空闲时尝试窃取其他线程的任务。

#include <iostream>
#include <vector>
#include <mutex>

std::mutex cout_mutex;

void test_work_stealing() {
    const int THREAD_NUM = 4;
    const int TASK_NUM = 1000;
    std::vector<std::thread> threads;
    std::vector<WorkStealingQueue> queues(THREAD_NUM);

    // 初始化任务,全部放到第一个线程的队列中
    for (int i = 0; i < TASK_NUM; ++i) {
        int task_id = i;
        queues[0].push([task_id]() {
            std::lock_guard<std::mutex> lock(cout_mutex);
            std::cout << "Task " << task_id << " executed by thread " << std::this_thread::get_id() << std::endl;
        });
    }

    // 启动工作线程
    for (int i = 0; i < THREAD_NUM; ++i) {
        threads.emplace_back([i, &queues]() {
            Task task;
            // 不断尝试从本地队列取任务,取不到就尝试窃取其他线程的任务
            while (true) {
                // 先尝试本地弹出
                if (queues[i].pop(task)) {
                    task();
                    continue;
                }
                // 本地没有任务,尝试窃取其他线程的任务
                bool stolen = false;
                for (int j = 0; j < queues.size(); ++j) {
                    if (j == i) continue;
                    if (queues[j].steal(task)) {
                        task();
                        stolen = true;
                        break;
                    }
                }
                if (!stolen) {
                    // 所有队列都没有任务,退出循环(实际场景可加休眠或退出条件)
                    break;
                }
            }
        });
    }

    // 等待所有线程结束
    for (auto& t : threads) {
        t.join();
    }
}

int main() {
    test_work_stealing();
    return 0;
}

性能优化要点

  • 队列容量设置为2的幂次,用位运算代替取模操作,提升计算效率
  • 头尾索引使用原子变量,避免互斥锁开销,本地操作和窃取操作操作不同的索引,减少竞争
  • 增加缓存行填充,避免head和tail两个原子变量处于同一个缓存行,减少伪共享带来的性能损耗
  • 任务推送和弹出使用合适的内存序,在保证正确性的前提下降低内存屏障的开销
  • 窃取操作使用CAS而不是互斥锁,多个窃取线程竞争时开销更小

注意事项

工作窃取队列适合任务粒度较小的场景,如果任务执行时间很长,窃取带来的收益会被任务执行时间掩盖。另外,任务对象需要支持移动语义,避免任务拷贝带来的开销。如果队列需要动态扩容,要额外处理扩容时的并发安全问题,或者提前预估任务数量设置合适的初始容量。

C++多线程work_stealing并发调优无锁队列修改时间:2026-07-13 20:54:40

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。