线程池项目设计文档
项目结构
threadpool/
├── CMakeLists.txt
├── include/
│ ├── TaskQueue.hpp # 任务队列(普通 deque + 优先队列)+ 工作窃取取任务逻辑
│ └── ThreadPool.hpp # 4 种线程池:Fixed / Cache / Signal / Scheduled / WorkStealing
├── main/
│ ├── CMakeLists.txt
│ └── test.cpp
└── build/
编译
bash
cd build && cmake .. && make
产出二进制位于 bin/test。
头文件依赖关系
ThreadPool.hpp 直接 #include "TaskQueue.hpp"。TaskQueue.hpp 必须自包含 #include <functional>,因为 using Task = std::function<void(void)>; 需要它,否则一旦 TaskQueue.hpp 被先于 <functional> include,整个文件会因为 Task 未声明而出现一堆连锁报错。
〇、设计思考原文
缓存线程池相比于固定线程池,那就是线程空闲要回收。那么问题就是你线程抢的时候都不知道是哪个线程抢的,那你就肯定要专门去存储它的 ID。它的线程返回它的线程 ID,或者用一个队列去存储它的线程 ID 来进行回收。那回收的时候那肯定是要用另一个专门的管理线程来进行扩容线程和删除线程吧。比如主线程一般都是添加任务,那么另一个线程那就是管理线程,它需要时刻地去监控,或者是采用轮巡的方式来判断是否需要扩容,或者是说是否需要去回收线程。但是时刻都是要不断运行的,又不像其他工作线程一样,它可以不达到条件就被阻塞去进行睡眠。但是管理线程不允许,管理线程要反复地去监管监视,要一直醒着一直活着,然后创建线程,无非就是在线程数组里面加一个嘛。那么回收线程最麻烦的就是当前线程它退出的时候,你要返回它的线程 ID,还要把自身的 ID 插入到那个睡眠或者是空闲线程数组里面,它还要知道自己在线程数组里是哪里,因为当它被回收的时候,要把自己从线程数组里面删除,所以它还要知道自己在线程数组里是在哪里,这是需要两个处理的关键点。
解决位置这个问题呢,就是采用先创建线程的时候,线程自身它可以返回自己的 ID,然后在线程数组里面从外部线程对象本身又可以返回它的 ID,一个叫 get_id,就是外部调用这个线程对象,然后去访问它的 get_id 函数就可以返回你要看的外部的那个线程的 ID 号是多少;然后你在当前线程里面,你可以返回出去一个 ID,就是当前线程你还在运行的时候,但是你要使用的是叫 std::this_thread::get_id()。也就是说,删除线程,它们唯一的链接点就是它的 ID 号,调用线程类里面自带的 ID 号进行删除。
也就是说工作窃取线程池,难就难在任务怎么加、任务怎么分配、怎么窃取、怎么去让一个线程访问到其他线程的队列、怎么拿过来。
那本来多线程函数创建的时候都是要调用一个条件变量来让它们进入睡眠状态,等到有任务进入到任务队列的时候,才会去争抢这把锁,才会把它们唤醒,去抢着执行这个线程。但是任务一空它们又进入睡眠。但是那假设要抢任务的时候,那这个条件变量又该怎么去写呢?又可以让它去抢任务,再而且抢任务是一种方式;那假设所有的线程的任务队列都为空,那这又是一种什么情况?
以及创建多个线程的时候,因为多个线程首先都会通过窃取其他线程来判断是否属于初始状态。然后这个时候,因为它要去访问其他相邻的线程数组。但是我们写的是容器,容器可能会扩容。如果说我们边启动线程边扩容的话,如果说位置变了的话,那么原有的地址就失效了。所以说,我们应该是统一先创建好之后确定了最终的地址,再去传进去,这样再去创建线程,这样就不会出现潜在的问题。
解决方法就是去获取 b 锁的时候,先把 a 锁释放掉,再去访问 b 锁。
(下面源码中把这些思考点嵌到了对应的代码位置作为注释。)
一、TaskQueue.hpp 完整源码
cpp
#ifndef TASKQUEUE_HPP
#define TASKQUEUE_HPP
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <deque>
#include <functional>
#include <iostream>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include <utility>
#include <vector>
namespace ctx {
struct workers;
static const int MaxTaskCount = 200;
using Task = std::function<void(
void)>; // cs项目中我们是用c的方法写了一个线程的入口函数,线程是线程,任务是任务,
// 这里我们直接将任务本身当作线程
template <
class
T> // 函数模板不仅可以传入变量,用户自定义类型,甚至函数包装器也能够传递进来。
class Taskdeque {
private:
std::deque<T> m_queue;
mutable std::mutex m_mutex;
mutable std::condition_variable m_notEmpty;
mutable std::condition_variable m_notFull;
int m_maxSize;
std::atomic<bool> status;
bool IsFull() const { return m_queue.size() >= m_maxSize; }
bool IsEmpty() const { return m_queue.empty(); }
template <class F> bool Add(F &&task) {
if (status) {
std::unique_lock<std::mutex> locker(m_mutex);
m_notFull.wait(locker,
[this]() -> bool { return !IsFull() || !status.load(); });
if (!status.load()) {
return false;
}
m_queue.push_back(std::forward<F>(task));
m_notEmpty.notify_all();
return true;
}
std::cout << "task deque status = false" << std::endl;
return false;
}
public:
Taskdeque(int maxsize = MaxTaskCount) : m_maxSize(maxsize), status(true) {}
~Taskdeque() { Stop(); }
Taskdeque(const Taskdeque &) = delete;
Taskdeque &operator=(const Taskdeque &) = delete;
bool Put(const T &task) { return Add(task); }
bool Put(T &&task) { return Add(std::forward<T>(task)); }
bool Take(T *ptask) {
if (status) {
if (nullptr == ptask)
return false;
std::unique_lock<std::mutex> locker(m_mutex);
m_notEmpty.wait(
locker, [this]() -> bool { return !IsEmpty() || !status.load(); });
if (m_queue.empty() && !status.load()) {
return false;
}
*ptask = m_queue.front();
m_queue.pop_front();
m_notFull.notify_all();
return true;
}
std::cout << "task deque status = false" << std::endl;
return false;
}
// 特殊版本take只针对于工作窃取线程池中的特殊情况
bool not_cv_Take(T *ptask) {
if (status) {
if (nullptr == ptask)
return false;
std::unique_lock<std::mutex> locker(m_mutex);
if (m_queue.empty() || !status.load()) {
return false;
}
*ptask = m_queue.front();
m_queue.pop_front();
m_notFull.notify_all();
return true;
}
std::cout << "task deque status = false" << std::endl;
return false;
}
bool Take(std::deque<T> *pdeq) {
if (status) {
if (nullptr == pdeq)
return false;
std::unique_lock<std::mutex> locker(m_mutex);
m_notEmpty.wait(
locker, [this]() -> bool { return !IsEmpty() || !status.load(); });
*pdeq = std::move(m_queue);
m_notFull.notify_all();
return true;
}
std::cout << "task deque status = false" << std::endl;
return false;
}
int Cache_Take(T *ptask, std::chrono::milliseconds timeout) {
if (status) {
if (nullptr == ptask)
return -1;
std::unique_lock<std::mutex> locker(m_mutex);
bool result = m_notEmpty.wait_for(
locker, timeout, [this]() -> bool { return !IsEmpty() || !status; });
if (!status) {
return -2;
}
if (result == false) {
return 0;
}
*ptask = m_queue.front();
m_queue.pop_front();
m_notFull.notify_all();
return 1;
}
std::cout << "task deque status = false" << std::endl;
return -2;
}
int Cache_Take(std::deque<T> *pdeq, std::chrono::milliseconds timeout) {
if (status) {
if (nullptr == pdeq)
return -1;
std::unique_lock<std::mutex> locker(m_mutex);
bool results = m_notEmpty.wait_for(
locker, timeout, [this]() -> bool { return !IsEmpty() || !status; });
if (!status) {
return -2;
}
if (results == false) {
return 0;
}
*pdeq = std::move(m_queue);
m_notFull.notify_all();
return 1;
}
std::cout << "task deque status = false" << std::endl;
return -2;
}
bool Stealling_Take(T *ptask, std::vector<std::shared_ptr<workers>> *b);
bool Stealling_Take(std::deque<T> *pdeq) {
if (status) {
if (nullptr == pdeq)
return false;
std::unique_lock<std::mutex> locker(m_mutex);
m_notEmpty.wait(
locker, [this]() -> bool { return !IsEmpty() || !status.load(); });
*pdeq = std::move(m_queue);
m_notFull.notify_all();
return true;
}
std::cout << "task deque status = false" << std::endl;
return false;
}
void Stop() {
{
std::unique_lock<std::mutex> locker(m_mutex);
status = false;
}
m_notEmpty.notify_all();
m_notFull.notify_all();
}
bool Full() const {
std::unique_lock<std::mutex> locker(m_mutex);
return IsFull();
}
bool Empty() const {
std::unique_lock<std::mutex> locker(m_mutex);
return IsEmpty();
}
size_t Size() const {
std::unique_lock<std::mutex> locker(m_mutex);
return m_queue.size();
}
size_t Count() const { return m_maxSize; }
bool RE_STATUS() const { return status; };
};
struct workers {
std::thread worker_thread;
std::unique_ptr<Taskdeque<Task>> Taskdeque_address;
};
template <class T>
bool Taskdeque<T>::Stealling_Take(T *ptask, std::vector<std::shared_ptr<workers>> *b) {
if (status) {
if (nullptr == ptask)
return false;
std::unique_lock<std::mutex> locker(m_mutex);
while (status.load()) {
// 1. 自己队列非空就直接取,省得再绕去偷别人
if (!m_queue.empty()) {
*ptask = std::move(m_queue.front());
m_queue.pop_front();
m_notFull.notify_all();
return true;
}
// 2. 释放自己的锁再去偷别人,避免持自己锁去 lock 别人引发 AB-BA 死锁
locker.unlock();
for (auto i = (*b).begin(); i != (*b).end(); ++i) {
if ((*i)->worker_thread.get_id() == std::this_thread::get_id())
continue; // 跳过自己
if (!(*i)->Taskdeque_address->RE_STATUS())
continue;
T task;
if ((*i)->Taskdeque_address->not_cv_Take(&task)) {
// 偷到直接返回,不再 Put 进自己队列------否则会再次 lock m_mutex 自死锁
*ptask = std::move(task);
return true;
}
}
// 3. 都没偷到,重新持锁 wait 一小段时间。
// 用 wait_for 而不是 wait:如果任务被 AddTask 塞到了别人的队列,
// 唤醒的是别人队列的 m_notEmpty,本线程听不到;超时一段时间后醒来
// 自己重新遍历去偷,避免错过别人队列里的可偷任务。
locker.lock();
m_notEmpty.wait_for(locker, std::chrono::milliseconds(50),
[this]() -> bool { return !m_queue.empty() || !status.load(); });
// 超时或被唤醒后回到 while 顶部:先看自己队列,没有再去偷
}
}
std::cout << "task deque status = false" << std::endl;
return false;
}
} // namespace ctx
namespace ctx {
#define DELAY 600
struct ScheduledTask {
Task task;
std::chrono::steady_clock::time_point execute_time;
};
struct CompareTask {
bool operator()(const ScheduledTask &a, const ScheduledTask &b) const {
return a.execute_time > b.execute_time;
}
};
class PriorityTaskqueue {
private:
std::priority_queue<ScheduledTask, std::vector<ScheduledTask>, CompareTask>
m_queue; // 从做到右依次是元素类型,底层容器类型以及比较函数
mutable std::mutex m_mutex;
mutable std::condition_variable m_notEmpty;
mutable std::condition_variable m_notFull;
int m_maxSize;
std::atomic<bool> status;
bool IsFull() const { return m_queue.size() >= m_maxSize; }
bool IsEmpty() const { return m_queue.empty(); }
template <class F> bool Add(F &&task) {
if (status) {
std::unique_lock<std::mutex> locker(m_mutex);
m_notFull.wait(locker,
[this]() -> bool { return !IsFull() || !status.load(); });
if (!status.load()) {
return false;
}
m_queue.push(std::forward<F>(task));
m_notEmpty.notify_all();
return true;
}
std::cout << "task deque status = false" << std::endl;
return false;
}
public:
PriorityTaskqueue(int maxsize = MaxTaskCount)
: m_maxSize(maxsize), status(true) {}
~PriorityTaskqueue() { Stop(); }
PriorityTaskqueue(const PriorityTaskqueue &) = delete;
PriorityTaskqueue &operator=(const PriorityTaskqueue &) = delete;
bool Put(const ScheduledTask &task) { return Add(task); }
bool Put(ScheduledTask &&task) {
return Add(std::forward<ScheduledTask>(task));
}
int Cache_Take(
ScheduledTask *ptask,
std::chrono::milliseconds
timeout) { // 这里的设计思路是到我们时间才去取这个任务,取完任务立即执行,
if (nullptr == ptask) {
return -1;
}
while (status) {
std::unique_lock<std::mutex> locker(m_mutex);
bool result = m_notEmpty.wait_for(
locker, timeout, [this]() -> bool { return !IsEmpty() || !status; });
if (!status) {
return -2;
}
if (result == false) {
return 0;
}
auto time = m_queue.top().execute_time;
// 因为我们设计线程的逻辑是,一个线程每一次只执行一个任务,所以我们顺利到达或超过时间点的时候取出任务,然后立刻退出这个取任务的函数,
// 所以能直接取任务的是特殊情况,大部分是需要睡眠一段时间并醒来重新检查的,所以取出任务要单独写一个if语句作为特殊情况的处理
if (time <= std::chrono::steady_clock::now()) {
*ptask = m_queue.top();
m_queue.pop();
m_notFull.notify_all();
return 1;
}
m_notEmpty.wait_until(
locker,
time); // wait_until第二个参数是绝对时间点,到达这一时刻就唤醒线程。
// 在睡眠的时候会释放锁,但是释放锁的时候可能会有其他线程在加任务,或者去取任务,加任务可能又有更早的任务到达了队头,所以醒来之后重新判断一遍,所以要用while循环
}
std::cout << "task priotity_queue status = false" << std::endl;
return -2;
}
// 此时可能发生的情况:
#if 0
线程 A 拿到锁,看到队头任务的 execute_time 还没到 → 调 wait_until → 释放锁,睡眠
线程 B 拿到锁(A 释放了),看到同一个队头任务,时间也没到 → 也调 wait_until → 释放锁,睡眠
时间到了 → A、B 同时被唤醒(同一个条件变量 m_notEmpty)
两个线程抢锁,假设 A 先抢到 → 循环回去 → wait_for 发现队列非空 → 看时间到了 → pop 取走任务 → 返回 1
B 抢到锁 → 循环回去 → wait_for 发现队列可能空了 → 如果空了就重新等新任务,如果有别的任务就看那个任务的时间
还有一种可能(队列空不空原理都一样的):
A、B 都在 wait_until 睡眠,等待同一个任务的 execute_time
时间到了,A、B 被唤醒,尝试重新抢锁
但此时 线程 C 刚执行完上一个任务,调 Cache_Take 也来抢锁
C 先抢到锁 → wait_for 发现队列非空 → 看时间已到 → pop 取走任务 → 返回 1
A 抢到锁 → 循环回去 → wait_for 发现队列空了 → 重新等新任务
B 抢到锁 → 同样,队列空了 → 重新等新任务
中间态的竞争的线程有哪些:
在任务不止一个的条件下:
线程少的时候:所有线程很快都进入 wait_until,时间点到时只有 wait_until 线程被唤醒竞争
线程足够多时:抢锁过程本身消耗的时间不可忽略,execute_time 可能在部分线程还没走完流程时就到达,导致 wait_until 线程、阻塞在 mutex 上的线程、以及 wait_for 中的线程混合竞争
#endif
int Cache_Take(std::priority_queue<ScheduledTask, std::vector<ScheduledTask>,
CompareTask> *pdeq,
std::chrono::milliseconds timeout) {
if (status) {
if (nullptr == pdeq)
return -1;
std::unique_lock<std::mutex> locker(m_mutex);
bool results = m_notEmpty.wait_for(
locker, timeout, [this]() -> bool { return !IsEmpty() || !status; });
if (!status) {
return -2;
}
if (results == false) {
return 0;
}
*pdeq = std::move(m_queue);
m_notFull.notify_all();
return 1;
}
std::cout << "task priotity_queue status = false" << std::endl;
return -2;
}
void Stop() {
{
std::unique_lock<std::mutex> locker(m_mutex);
status = false;
}
m_notEmpty.notify_all();
m_notFull.notify_all();
}
bool Full() const {
std::unique_lock<std::mutex> locker(m_mutex);
return IsFull();
}
bool Empty() const {
std::unique_lock<std::mutex> locker(m_mutex);
return IsEmpty();
}
size_t Size() const {
std::unique_lock<std::mutex> locker(m_mutex);
return m_queue.size();
}
size_t Count() const { return m_maxSize; }
};
} // namespace ctx
#endif
二、ThreadPool.hpp 完整源码
cpp
#ifndef FIXEDTHREADPOOL_HPP
#define FIXEDTHREADPOOL_HPP
#include "TaskQueue.hpp"
#include <atomic>
#include <chrono>
#include <functional>
#include <list>
#include <memory>
#include <mutex>
#include <queue>
#include <utility>
//线程函数能访问什么取决于这个可调用对象本身的作用域/捕获/接收的指针,而不是取决于线程跑起来之后运行的线程作用域。
namespace ctx {
class FixedThreadPool {
private:
std::list<std::shared_ptr<std::thread>> m_threadsgroup;
ctx::Taskdeque<Task> m_taskqueue;
std::atomic<bool> m_running;
std::once_flag m_flag; // 意义是确保工作线程只被创建一次,并且是线程安全的。
int thread_num;
int deque_size;
void Start(int thread_num) {
this->thread_num = thread_num;
m_running = true;
std::call_once(m_flag, [this, thread_num] {
for (int i = 0; i < thread_num; i++) {
m_threadsgroup.emplace_back(
std::make_shared<std::thread>(&FixedThreadPool::RuninThread, this));
// 共享指针这样传参数,完成了线程初始化以及赋值两个步骤:
// 第一步创建线程,将小括号内的两个参数传递给thread线程类进行初始化,初始化成功即成功创建了一个线程。
// std::thread(&FixedThreadPool::RuninThread, this);这是thread的初始化。
// 第二步,将这个线程交给这个共享指针进行管理。
// 所以参数这么写的原因,是因为线程要执行的函数入口,是一个类里面的函数指针,我们要让这个线程知道这个入口在哪里,即当前类对象的首地址,加上偏移量
// 所以我们要传入当前线程池的地址加入它的偏移量,注意参数位置互换一下。
}
});
}
void RuninThread() {
while (m_running) {
Task task;
if (!m_taskqueue.Take(&task)) {
break; // 设计的时候能失败只能说明任务队列未被启用,所以只能退出线程,因为无法从任务队列取出任务。
}
task();
}
}
void ForceStopThreadGroup() {
m_running = false;
m_taskqueue.Stop();
for (auto it = m_threadsgroup.begin(); it != m_threadsgroup.end(); it++) {
if ((*it)->joinable()) {
(*it)->join();
}
}
}
public:
FixedThreadPool(int deque_size,
int numthread = std::thread::hardware_concurrency())
: deque_size(deque_size) {
Start(numthread);
}
~FixedThreadPool() { Stop(); }
void Stop() { ForceStopThreadGroup(); }
void AddTask(const Task &task) {
if (!m_taskqueue.Put(task)) {
std::cout << "AddTask(const Task&) false" << std::endl;
}
}
void AddTask(Task &&task) {
if (!m_taskqueue.Put(std::forward<Task &&>(task))) {
std::cout << "AddTask(Task&&) false" << std::endl;
}
}
};
} // namespace ctx
namespace ctx {
#define MINITHREADNUM 1
#define TIMEOUT 60
struct Worker {
std::thread thread;
std::atomic<bool> busy{false};
Worker() = default; // 表示使用编译器原生的构造函数
// 效果类似于Worker() : thread(), busy(false) {}
};
// ============================================================================
// CacheThreadPool 设计要点(设计思考)
// ----------------------------------------------------------------------------
// 相比于固定线程池,核心区别就是:线程空闲要回收。
// 难点:线程抢任务的时候你不知道是哪个线程抢到的,所以肯定要专门去存储它的
// ID------用一个队列去存储空闲线程的线程ID来进行回收。
// 回收的时候肯定要用另一个专门的管理线程(sudo)来进行扩容线程和删除线程。
// 主线程一般是添加任务,那么另一个线程就是管理线程,它需要时刻去监控,
// 或者采用轮巡的方式来判断是否需要扩容、是否需要回收线程。
// 但是工作线程(管理线程)不允许像普通工作线程那样不达条件就 wait 进睡眠,
// 管理线程要反复去监管监视,要一直醒着一直活着,然后创建线程、回收线程。
//
// 创建线程:无非就是在线程数组里面加一个 worker 嘛。
// 回收线程最麻烦的是:当前线程退出的时候要返回它的线程ID,还要把自身的ID
// 插入到那个空闲线程ID队列里面。它还要知道自己在线程数组里面是在哪里------
// 因为当它被回收的时候要把自己从线程数组里面删除。这是两个需要处理的关键点。
//
// 解决"位置"问题:
// - 创建线程时,线程自身可以返回自己的 ID(std::this_thread::get_id())
// - 在线程数组里,从外部线程对象本身又可以返回它的 ID
// (worker->thread.get_id() ------外部调用这个线程对象,访问它的 get_id
// 函数就可以返回你要看的那个外部线程的 ID 号是多少)
// - 删除线程时,它们唯一的链接点就是它的 ID 号------调用线程类里面自带的
// ID 号进行删除。
// ============================================================================
class CacheThreadPool {
private:
std::list<std::shared_ptr<Worker>> m_threadsgroup;
ctx::Taskdeque<Task> m_taskqueue;
std::atomic<bool> m_running;
// a开头的是管理空闲线程id
// 难点:线程抢的时候不知道是哪个线程抢的,所以专门用一个队列存储它的 ID
// 以便后续回收------下面这个 m_exit_threads_id 就是这个用途。
std::mutex a_mutex;
std::queue<std::thread::id> m_exit_threads_id;
// b开头管理的是线程数组
std::mutex b_mutex;
// 管理线程
// 主线程添加任务,这个 sudo 线程专门负责"管理":扩容和回收。
// 管理线程要一直醒着一直活着,不能像工作线程那样 wait 进睡眠------下面 Sudo()
// 用 yield() 而不是 wait,就是这个原因。
std::thread sudo;
int minithread_num;
int maxthread_num;
int deque_size;
std::atomic<int> thread_num_now{0}; // 当前创建的子线程数
std::chrono::milliseconds
timeout; // 超时相当于设置了一段闹钟,过了一段时间就会让线程从wait的队列中醒来
void Start(int thread_num) {
this->maxthread_num = thread_num;
m_running = true;
for (int i = 0; i < thread_num_now; i++) {
auto worker = std::make_shared<Worker>();
Worker *self =
worker.get(); // 知道自己的位置在哪里并传递给线程函数好让他改变状态。
worker->thread = std::thread(&CacheThreadPool::RuninThread, this, self);
{
std::lock_guard<std::mutex> lock(b_mutex);
m_threadsgroup.emplace_back(std::move(worker));
}
}
this->sudo = std::thread(&CacheThreadPool::Sudo, this);
}
void RuninThread(Worker *self) {
while (m_running) {
Task task;
int result = m_taskqueue.Cache_Take(&task, timeout);
if (result == 1) {
self->busy = true;
task();
self->busy = false;
} else if (result == 0) {
// 超时没拿到任务 → 当前线程准备退出。
// 关键点1:当前线程退出时要返回它的线程ID,并把自身ID插入到空闲线程
// ID队列里,让管理线程 sudo 后续来处理它的回收。
self->busy = false;
{
std::lock_guard<std::mutex> lock(a_mutex);
m_exit_threads_id.push(std::this_thread::get_id());
}
return;
} else {
return;
}
}
}
void AddThread() {
if (thread_num_now >= maxthread_num || m_running == false) {
return;
}
{
std::unique_lock<std::mutex> locker(b_mutex);
++thread_num_now;
auto worker = std::make_shared<Worker>();
Worker *self = worker.get();
worker->thread = std::thread(&CacheThreadPool::RuninThread, this, self);
m_threadsgroup.emplace_back(std::move(worker));
}
}
// 关键点2:被回收的时候要把自己从线程数组里删除------
// 但工作线程自己不知道自己在线程数组哪个位置,唯一链接点就是它的线程ID号:
// - 工作线程自己用 std::this_thread::get_id() 把自己ID塞进 m_exit_threads_id
// - 管理线程从 m_exit_threads_id 取出 ID,再去 m_threadsgroup 里用
// worker->thread.get_id() 跟它比对,匹配上的就是要回收的 worker
// - 然后 join + erase 它
bool CleanupThread() {
std::thread::id exit_id;
{
std::lock_guard<std::mutex> lock(a_mutex);
if (m_exit_threads_id.empty()) {
return false;
}
exit_id = m_exit_threads_id.front();
m_exit_threads_id.pop();
}
std::shared_ptr<Worker> target;
{
std::lock_guard<std::mutex> lock(b_mutex);
for (const auto &worker : m_threadsgroup) {
if (worker->thread.get_id() == exit_id) {
target = worker;
break;
}
}
}
if (!target) {
return false;
}
if (target->thread.joinable()) {
target->thread.join();
}
{
std::lock_guard<std::mutex> lock(b_mutex);
for (auto it = m_threadsgroup.begin(); it != m_threadsgroup.end(); ++it) {
if (*it == target) {
m_threadsgroup.erase(it);
--thread_num_now;
break;
}
}
}
return true;
}
// 管理线程要时刻监控,采用轮巡的方式判断是否需要扩容、是否需要回收线程。
// 注意:管理线程不能用 wait 进睡眠------必须一直醒着一直活着------
// 没事干的时候用 yield() 让出 CPU,而不是 wait。
void Sudo() { // 但是线程有空闲的情况属于正常情况
while (m_running) {
if (CleanupThread()) {
continue;
}
if (!m_taskqueue.Empty()) {
AddThread();
} else {
std::this_thread::
yield(); // 当前线程主动让出CPU使用权,提示操作系统可以先运行其他线程,什么时候调用它取决于操作系统
// 是空循环的一种优化方案。
}
}
}
void ForceStopThreadGroup() {
m_running = false;
m_taskqueue.Stop();
std::unique_lock<std::mutex> locker(b_mutex);
for (auto it = m_threadsgroup.begin(); it != m_threadsgroup.end();) {
if ((*it)->thread.joinable()) {
(*it)->thread.join();
--thread_num_now;
it = m_threadsgroup.erase(
it); // erase本身会返回删除元素后的下一个迭代器。
}
}
}
public:
CacheThreadPool(int deque_size, int thread_num_now = MINITHREADNUM,
int minithread_num = MINITHREADNUM,
int maxnumthread = std::thread::hardware_concurrency())
: deque_size(deque_size), thread_num_now(thread_num_now),
minithread_num(minithread_num) {
timeout = std::chrono::milliseconds(TIMEOUT);
Start(maxnumthread);
}
~CacheThreadPool() {
Stop();
if (sudo.joinable()) {
sudo.join();
}
}
void Stop() { ForceStopThreadGroup(); }
void AddTask(const Task &task) {
if (!m_taskqueue.Put(task)) {
std::cout << "AddTask(const Task&) false" << std::endl;
}
}
void AddTask(Task &&task) {
if (!m_taskqueue.Put(std::forward<Task &&>(task))) {
std::cout << "AddTask(Task&&) false" << std::endl;
}
}
};
} // namespace ctx
namespace ctx {
class SignalThreadPool {
private:
ctx::Taskdeque<Task> m_taskqueue;
std::atomic<bool> m_running{true};
std::thread work;
int deque_size;
void RuninThread() {
while (m_running) {
Task task;
if (!m_taskqueue.Take(&task)) {
break;
}
task();
}
}
void ForceStopThreadGroup() {
m_running = false;
m_taskqueue.Stop();
if (work.joinable()) {
work.join();
}
}
public:
SignalThreadPool(int deque_size)
: deque_size(deque_size), m_taskqueue(deque_size) {
this->work = std::thread(&SignalThreadPool::RuninThread, this);
}
~SignalThreadPool() { Stop(); }
void Stop() { ForceStopThreadGroup(); }
void AddTask(const Task &task) {
if (!m_taskqueue.Put(task)) {
std::cout << "AddTask(const Task&) false" << std::endl;
}
}
void AddTask(Task &&task) {
if (!m_taskqueue.Put(std::forward<Task &&>(task))) {
std::cout << "AddTask(Task&&) false" << std::endl;
}
}
};
} // namespace ctx
namespace ctx {
class
ScheduledThreadPool { // 设计思路就是,除了取任务的时候可能会休眠,像其他的添加任务,执行任务,以及创建销毁线程都是立即执行,不受任何的影响。
private:
std::list<std::shared_ptr<Worker>> m_threadsgroup;
ctx::PriorityTaskqueue task_p_q;
std::atomic<bool> m_running;
// a开头的是管理空闲线程id
std::mutex a_mutex;
std::queue<std::thread::id> m_exit_threads_id;
// b开头管理的是线程数组
std::mutex b_mutex;
// 管理线程
std::thread sudo;
int minithread_num;
int maxthread_num;
int deque_size;
std::atomic<int> thread_num_now{0}; // 当前创建的子线程数
std::chrono::milliseconds
timeout; // 超时相当于设置了一段闹钟,过了一段时间就会让线程从wait的队列中醒来
void Start(int thread_num) {
this->maxthread_num = thread_num;
m_running = true;
for (int i = 0; i < thread_num_now; i++) {
auto worker = std::make_shared<Worker>();
Worker *self =
worker.get(); // 知道自己的位置在哪里并传递给线程函数好让他改变状态。
worker->thread =
std::thread(&ScheduledThreadPool::RuninThread, this, self);
{
std::lock_guard<std::mutex> lock(b_mutex);
m_threadsgroup.emplace_back(std::move(worker));
}
}
this->sudo = std::thread(&ScheduledThreadPool::Sudo, this);
}
void RuninThread(Worker *self) {
while (m_running) {
ScheduledTask task;
int result = task_p_q.Cache_Take(&task, timeout);
if (result == 1) {
self->busy = true;
task.task();
self->busy = false;
} else if (result == 0) {
self->busy = false;
{
std::lock_guard<std::mutex> lock(a_mutex);
m_exit_threads_id.push(std::this_thread::get_id());
}
return;
} else {
return;
}
}
}
void AddThread() {
if (thread_num_now >= maxthread_num || m_running == false) {
return;
}
{
std::unique_lock<std::mutex> locker(b_mutex);
++thread_num_now;
auto worker = std::make_shared<Worker>();
Worker *self = worker.get();
worker->thread =
std::thread(&ScheduledThreadPool::RuninThread, this, self);
m_threadsgroup.emplace_back(std::move(worker));
}
}
bool CleanupThread() {
std::thread::id exit_id;
{
std::lock_guard<std::mutex> lock(a_mutex);
if (m_exit_threads_id.empty()) {
return false;
}
exit_id = m_exit_threads_id.front();
m_exit_threads_id.pop();
}
std::shared_ptr<Worker> target;
{
std::lock_guard<std::mutex> lock(b_mutex);
for (const auto &worker : m_threadsgroup) {
if (worker->thread.get_id() == exit_id) {
target = worker;
break;
}
}
}
if (!target) {
return false;
}
if (target->thread.joinable()) {
target->thread.join();
}
{
std::lock_guard<std::mutex> lock(b_mutex);
for (auto it = m_threadsgroup.begin(); it != m_threadsgroup.end(); ++it) {
if (*it == target) {
m_threadsgroup.erase(it);
--thread_num_now;
break;
}
}
}
return true;
}
void Sudo() { // 但是线程有空闲的情况属于正常情况
while (m_running) {
if (CleanupThread()) {
continue;
}
if (!task_p_q.Empty()) {
AddThread();
} else {
std::this_thread::
yield(); // 当前线程主动让出CPU使用权,提示操作系统可以先运行其他线程,什么时候调用它取决于操作系统
// 是空循环的一种优化方案。
}
}
}
void ForceStopThreadGroup() {
m_running = false;
task_p_q.Stop();
std::unique_lock<std::mutex> locker(b_mutex);
for (auto it = m_threadsgroup.begin(); it != m_threadsgroup.end();) {
if ((*it)->thread.joinable()) {
(*it)->thread.join();
--thread_num_now;
it = m_threadsgroup.erase(
it); // erase本身会返回删除元素后的下一个迭代器。
}
}
}
public:
ScheduledThreadPool(int deque_size, int thread_num_now = MINITHREADNUM,
int minithread_num = MINITHREADNUM,
int maxnumthread = std::thread::hardware_concurrency())
: deque_size(deque_size), thread_num_now(thread_num_now),
task_p_q(deque_size), minithread_num(minithread_num) {
timeout = std::chrono::milliseconds(TIMEOUT);
Start(maxnumthread);
}
~ScheduledThreadPool() {
Stop();
if (sudo.joinable()) {
sudo.join();
}
}
void Stop() { ForceStopThreadGroup(); }
void AddTask(const Task &task, std::chrono::milliseconds delay) {
struct ScheduledTask a;
a.execute_time = std::chrono::steady_clock::now() +
delay; // 获取当前机器时间加上延迟时间
a.task = std::move(task);
if (!task_p_q.Put(a)) {
std::cout << "AddTask(const ScheduledTask&) false" << std::endl;
}
}
void AddTask(Task &&task, std::chrono::milliseconds delay) {
struct ScheduledTask a;
a.execute_time = std::chrono::steady_clock::now() +
delay; // 获取当前机器时间加上延迟时间
a.task = std::move(task);
if (!task_p_q.Put(std::forward<ScheduledTask &&>(a))) {
std::cout << "AddTask(ScheduledTask&&) false" << std::endl;
}
}
void AddTask(const ScheduledTask &task) {
if (!task_p_q.Put(task)) {
std::cout << "AddTask(const ScheduledTask&) false" << std::endl;
}
}
void AddTask(ScheduledTask &&task) {
if (!task_p_q.Put(std::forward<ScheduledTask &&>(task))) {
std::cout << "AddTask(ScheduledTask&&) false" << std::endl;
}
}
};
} // namespace ctx
#endif
namespace ctx {
// ============================================================================
// WorkStealingThreadPool 设计思考
// ----------------------------------------------------------------------------
// 工作窃取线程池,难就难在:
// - 任务怎么加?
// - 任务怎么分配?
// - 怎么窃取?
// - 怎么让一个线程访问到其他线程的队列?
// - 怎么把别人的任务拿过来?
//
// 关于条件变量:
// 多线程函数创建的时候都要调用一个条件变量来让它们进入睡眠状态,
// 等到有任务进入到任务队列的时候,才会去争抢这把锁,才会把它们唤醒去抢
// 着执行这个线程。任务一空它们又进入睡眠。
// 但是问题来了:那要抢任务的时候,这个条件变量又该怎么去写呢?
// - 抢任务(偷别人的任务)是一种方式
// - 假设所有线程的任务队列都为空,那这又是另一种情况
// (本实现采用 wait_for 短超时 + while 重试来覆盖这两种情况,详见
// TaskQueue.hpp 中的 Stealling_Take 实现)
//
// 关于创建多个线程时的启动顺序:
// 多个线程首先都会通过窃取其他线程来判断是否属于初始状态。然后这个时候,
// 因为它要去访问其他的相邻的线程数组。但是我们写的是容器,容器可能会扩
// 容。如果说我们边启动线程边扩容的话,位置变了的话,那么原有的地址就失
// 效了。所以,我们应该是统一先创建好之后确定了最终的地址,再去传进去,
// 这样再去创建线程,这样就不会出现潜在的问题。
// (详见下面 Start 函数里的实现)
//
// 关于 AB-BA 死锁的解决方法:
// 获取 b 锁的时候,先把 a 锁释放掉,再去访问 b 锁。
// (详见 TaskQueue.hpp 中 Stealling_Take 的 locker.unlock() 注释)
// ============================================================================
class WorkStealingThreadPool {
private:
std::vector<std::shared_ptr<workers>>
m_threadsgroup; // 这里改用vector方便下标访问
std::atomic<bool> m_running;
std::once_flag m_flag; // 意义是确保工作线程只被创建一次,并且是线程安全的。
// std::mutex m_mutex;//只有主线程在访问线程数组,可以不用加锁
int thread_num;
int deque_size;
std::atomic<int> index{0};
void Start(int thread_num) {
this->thread_num = thread_num;
m_running = true;
std::call_once(m_flag, [this, thread_num] {
m_threadsgroup.reserve(thread_num); // 预分配,避免扩容
// 一定要填充好队列之后再去启动线程,因为在扩容的时候,可能队列的起始
// 位置会发生改变,再加上我们自己设计的时候又要将这个队列传进去,如果
// 我们一边启动线程一边扩容,可能会出现加入队列的起始位置发生了变化,
// 线程启动的时候又回去遍历一遍线程数组判断是不是初始状态的时候,就遍
// 历了失效地址,所以一定分配好之后,最后确定了这个队列的最终位置后才
// 能传参进去启动线程。
for (int i = 0; i < thread_num; i++) {
auto tmp = std::make_shared<workers>();
tmp->Taskdeque_address.reset(new ctx::Taskdeque<Task>(deque_size));
m_threadsgroup.emplace_back(tmp);
}
for (int i = 0; i < thread_num; i++) {
m_threadsgroup[i]->worker_thread =
std::thread(&WorkStealingThreadPool::RuninThread, this,
m_threadsgroup[i], &m_threadsgroup);
}
});
}
void RuninThread(std::shared_ptr<workers> a,
std::vector<std::shared_ptr<workers>> *b) {
while (m_running) {
Task task;
if (!a->Taskdeque_address->Stealling_Take(&task, b)) {
continue;
}
task();
}
}
void ForceStopThreadGroup() {
m_running = false;
for (auto it = m_threadsgroup.begin(); it != m_threadsgroup.end(); it++) {
(*it)->Taskdeque_address->Stop();
if ((*it)->worker_thread.joinable()) {
(*it)->worker_thread.join();
}
}
}
public:
WorkStealingThreadPool(int deque_size,
int numthread = std::thread::hardware_concurrency())
: deque_size(deque_size) {
Start(numthread);
}
~WorkStealingThreadPool() { Stop(); }
void Stop() { ForceStopThreadGroup(); }
void AddTask(const Task &task) {
size_t i = index.fetch_add(1, std::memory_order_relaxed) % thread_num;
auto tmp = m_threadsgroup[i];
if (!tmp->Taskdeque_address->Put(task)) {
std::cout << i << " thread add false" << std::endl;
return;
}
}
void AddTask(Task &&task) {
size_t i = index.fetch_add(1, std::memory_order_relaxed) % thread_num;
auto tmp = m_threadsgroup[i];
if (!tmp->Taskdeque_address->Put(std::forward<Task &&>(task))) {
std::cout << i << " thread add false" << std::endl;
return;
}
}
};
} // namespace ctx