C++:(3) 线程的关联、条件变量、锁和线程池

1. 线程

1.1 使用示例

当线程函数为普通函数时:

cpp 复制代码
#include <iostream>
#include <thread>
 
void task(int x) {
}
 
int main() {
    std::thread t(task, 1); // 创建线程并运行 task 函数
    t.join();
    return 0;
}

当线程函数为非静态成员函数时,需要确保正确地传递 this 指针,以便成员函数能够在正确的对象上下文中被调用:

cpp 复制代码
// std::thread用于成员函数中(类内使用):
m_thread = std::thread(std::bind(&MyClasss::MemberFunction, this));
// std::thread用于非成员函数中(类外使用):
Myclass myClass;
thread = std::thread(std::bind(&MyClasss:: MemberFunction, myClass));

1.2 什么是线程的关联

  • 在 C++ 中,每个 std::thread 对象内部都持有一个 对操作系统底层线程的句柄(比如 POSIX 的 pthread_t 或 Windows 的线程句柄)。
  • 当一个 std::thread 对象 持有这个句柄,我们就说它 "关联"了一个实际运行的线程。
  • 关联 = 这个 std::thread 对象正在"代表"或"管理"一个真实的、由系统创建的线程。

例1:创建线程 → 关联

cpp 复制代码
std::thread t(foo);  // 系统创建了一个新线程执行 foo
// 此时 t "关联" 了这个新线程
// t.joinable() == true
  • t 内部保存了这个新线程的标识符(句柄)。
  • 它有责任后续调用 join() 或 detach() 来"处理"这个线程。

例2:默认构造 → 未关联:

cpp 复制代码
std::thread t;  // 没有创建任何线程
// t 不代表任何真实线程
// t.joinable() == false

例3:移动后 → 原对象不再关联

cpp 复制代码
std::thread t1(foo);
std::thread t2 = std::move(t1);  // t1 的"控制权"转移给 t2
// 现在:
// - t2 关联了那个线程 → t2.joinable() == true
// - t1 不再关联任何线程 → t1.joinable() == false

例4:调用 join() 后 → 不再关联

cpp 复制代码
std::thread t(foo);
t.join();  // 等待线程结束,并释放内部句柄
// 现在 t 不再关联任何线程
// t.joinable() == false

例5:调用 detach() 后 → 不再关联

cpp 复制代码
std::thread t(foo);
t.detach();  // 把线程"放飞",不再由 t 管理
// t 不再关联该线程(虽然线程还在跑)
// t.joinable() == false
  • std::thread 对象是否还拥有对某个底层线程的控制权(即是否需要你负责清理它)。
  • 如果 关联(joinable() == true)→ 必须调用 join() 或 detach(),否则析构时程序崩溃(std::terminate())。
  • 如果 未关联(joinable() == false)→ 它是个空壳,析构安全。

1.3 join 、joinable 与 detach

join():

  • 作用:阻塞当前线程(通常是主线程),直到被调用的 std::thread 对象所代表的线程执行完毕。
  • 目的:确保线程完成其任务后再继续执行后续代码,避免资源提前释放或数据竞争。
  • 调用条件 :只能对 可 join 的线程(即 joinable() 返回 true)调用 join()。
  • 副作用:调用 join() 后,该 std::thread 对象不再关联任何实际线程,变为 不可 join 状态(joinable() 变为 false)。

join用于让主线程等待子线程,主线程会卡在join直到子线程结束,之后才可继续执行主线程的代码,确保该线程在执行完之前主线程不会继续执行从而回收需要使用的资源。比如:

cpp 复制代码
#include <thread>
void workerFunction() {
    while(1){  // 执行工作
		}
}
int main() {
    std::thread worker(workerFunction); 
    return 0;
}

在worker线程中的while循环中刚开始执行工作,主线程就结束从而进程退出了。

加入worker.join( );使得主线程一直等待worker子线程执行完毕,而worker中是个死循环,故程序会一直执行其功能。

joinable():

  • 作用:检查 std::thread 对象是否关联了一个正在运行或尚未被 join()/detach() 的线程。
  • 返回值
    • true:表示该线程对象仍"拥有"一个底层线程(即未被 join 或 detach,也不是默认构造或已移动)。
    • false:表示没有关联的线程(例如:默认构造、已 join、已 detach、或已被移动到其他 thread 对象)。

joinable用于查询线程是否被join,没有则返回true。

在析构函数中,应确保线程被join以防止线程访问的类的资源被析构函数回收:

cpp 复制代码
MyClass::~MyClass() // 先join在关闭socket防止socket资源被回收
{
    if (m_ThreadPtr -> joinable()){
        m_ThreadPtr -> join();
    }
    if (close(m_socket_fd) < 0) {
        std::cerr << "关闭socket错误:"  << strerror(errno) << "!" << std::endl;
        std::exit(EXIT_FAILURE);
    }
}

detach():

detach() 的作用是 将 std::thread 对象与其所代表的底层线程"分离",使该线程在后台独立运行,不再受当前 std::thread 对象的控制。一旦调用 detach():

  • 该线程成为 "守护线程"(daemon thread) 或 "后台线程"。
  • 主线程(或其他线程)不再等待它结束。
  • std::thread 对象本身 不再关联任何线程(joinable() 变为 false)。
  • 程序退出时,不会自动等待 detached 线程完成。
  • 只能对 joinable() == true 的线程调用 detach()。
  • 否则会抛出 std::system_error 异常(例如对默认构造的线程调用 detach())。

例:

cpp 复制代码
std::thread t(task);
if (t.joinable()) {
    t.detach();       // ✅ 安全
}

join() vs detach() 对比:

特性 join() detach()
是否阻塞调用线程
线程结束后是否回收资源 是(由 join 完成) 是(由系统自动回收,但时机不确定)
能否访问线程局部变量 安全(等它结束) 危险(可能已销毁)
程序退出时行为 安全(已结束) 可能被强制终止
推荐程度 优先使用 谨慎使用

2.条件变量和锁

2.1 lock_guard

只能在构造时加锁,在析构时解锁,不可与条件变量配合使用。

  • 构造 lock_guard 时,会调用 mtx.lock()。
  • 析构 lock_guard 时(离开作用域),会自动调用 mtx.unlock()。
  • 不可手动解锁,也不可复制或移动(lock_guard 是不可拷贝、不可移动的)。

例:

cpp 复制代码
#include <mutex>
#include <iostream>

std::mutex mtx;

void safe_function() {
    std::lock_guard<std::mutex> lock(mtx); // 自动加锁
    // 临界区代码
    std::cout << "访问共享资源\n";
    // 函数结束时,lock_guard 析构,自动解锁
}

2.2 unique_lock

相比 std::lock_guard,它提供了延迟锁定、手动解锁、所有权转移、与条件变量配合等高级功能。

构造方式 行为
unique_lock<Mutex> lk(mtx); 立即加锁(默认)
unique_lock<Mutex> lk(mtx, std::defer_lock); 不立即加锁,延迟到后续调用 lk.lock()
unique_lock<Mutex> lk(mtx, std::try_to_lock); 尝试加锁(非阻塞),若失败则不持有锁
unique_lock<Mutex> lk(mtx, std::adopt_lock); 假设 mutex 已被当前线程锁定,unique_lock 接管所有权

延迟加锁示例:

cpp 复制代码
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// ... 做些其他事 ...
lock.lock();   // 手动加锁
// ... 临界区 ...
lock.unlock(); // 手动解锁(可选)

移动语义示例:

cpp 复制代码
std::unique_lock<std::mutex> lock1(mtx);
std::unique_lock<std::mutex> lock2 = std::move(lock1); // lock1 不再拥有锁
// 现在由 lock2 负责解锁

2.3 cv 和 notify

例:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
 
// 锁和条件变量需要在全局作用域或至少是线程间共享的作用域中声明,以便所有线程都能访问它们
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
 
void worker() {
    std::unique_lock<std::mutex> lock(mtx); // 加锁,如果被其他线程获取则阻塞
    cv.wait(lock, [] { return ready; });
    std::cout << "Worker thread is processing..." << std::endl;
    // 离开作用域时自动解锁
}
 
int main() {
    std::thread t(worker);
 
    {
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
        std::lock_guard<std::mutex> lock(mtx);// 加锁,如果被其他线程获取则阻塞
        ready = true; // 修改共享数据
    }
    cv.notify_one(); // 通知等待的线程
 
    t.join();
 
    return 0;
}

cv.wait(lock, [] { return ready; });

cv.wait()会检查lambda结果:

  1. 如果为 true,执行后续的代码;
  2. 如果为 false,释放mtx, 且阻塞直到其他线程调用 cv.notify_one() 或 cv.notify_all() 唤醒,被唤醒后,自动上锁,并再次检查lambda结果,重复以上步骤。

2.4 shared_lock 和 shared_mtx

std::shared_mutex 本身并不直接决定是共享锁还是独占锁,而是由std::shared_lock和std::unique_lock决定类型。共享锁和独占锁是互斥的,不能同时存在。

例:

cpp 复制代码
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
 
std::shared_mutex shared_mtx;
int shared_data = 0;
 
// 读操作(共享锁)
void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(shared_mtx); // 获取共享锁
    std::cout << "Reader " << id << " read data: " << shared_data << std::endl;
    // 离开作用域时自动释放共享锁
}
 
// 写操作(独占锁)
void writer(int value) {
    std::unique_lock<std::shared_mutex> lock(shared_mtx); // 获取独占锁
    shared_data = value;
    std::cout << "Writer updated data to: " << shared_data << std::endl;
    // 离开作用域时自动释放独占锁
}
 
int main() {
    std::vector<std::thread> threads;
 
    // 创建多个读线程
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back(reader, i);
    }
 
    // 创建一个写线程
    threads.emplace_back(writer, 42);
 
    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}
  1. std::unique_lock 对 std::shared_mutex 上锁时,其他任何线程包括std::shared_lock 都无法获取共享锁,只有在独占锁被释放后,其他线程才能获取共享锁。
  2. std::shared_lock对 std::shared_mutex 上锁时,其他线程仍然可以通过std::shared_lock获取共享锁std::shared_mutex,但std::unique_lock不可以获得独占锁std::shared_mutex。

3. 线程池

3.1 线程池的实现

thread_pool.h

cpp 复制代码
#pragma once
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <future>
#include "logger.h"

class ThreadPool {
public:
    /**
     * @brief 构造函数,创建指定数量的工作线程
     * @param numThreads 线程数量,建议为 std::thread::hardware_concurrency()
     */
    ThreadPool(size_t numThreads);
    
    /**
     * @brief 析构函数,等待所有任务完成并停止所有线程
     */
    ~ThreadPool();

    /**
     * @brief 向线程池提交任务
     * @tparam F 可调用对象类型(函数、lambda表达式、函数对象等)
     * @tparam Args 参数类型包
     * @param f 可调用对象
     * @param args 参数包
     * @return std::future 用于获取异步执行结果
     */
    template<class F, class... Args>
    auto enqueue(F&& f, Args&&... args) -> std::future<typename std::result_of<F(Args...)>::type>;

private:
    std::vector<std::thread> m_threads;           // 工作线程容器
    std::queue<std::function<void()>> m_tasks;    // 任务队列(存储无参数无返回值的可调用对象)
    
    std::mutex m_queueMutex;                      // 保护任务队列的互斥锁
    std::condition_variable m_cv;                 // 线程间同步的条件变量
    
    bool m_stop;                                  // 线程池停止标志
};

// 构造函数实现
inline ThreadPool::ThreadPool(size_t numThreads) : m_stop(false) {
    // 创建工作线程,每个线程都是任务的消费者
    for (size_t i = 0; i < numThreads; ++i) {
        m_threads.emplace_back([this] {
            // 线程主循环:不断从任务队列获取并执行任务
            while (true) {
                std::function<void()> task;  // 用于存储从队列中取出的任务
                
                {
                    // 使用unique_lock加锁,条件变量需要可解锁的锁类型
                    std::unique_lock<std::mutex> lock(m_queueMutex);
                    
                    // 等待条件满足:线程池停止或任务队列非空
                    // wait会原子地解锁并阻塞,被唤醒后重新加锁并检查条件
                    m_cv.wait(lock, [this] { 
                        return m_stop || !m_tasks.empty(); 
                    });
                    
                    // 退出条件:线程池已停止且所有任务已完成
                    if (m_stop && m_tasks.empty()) {
                        return;  // 线程函数返回,线程结束
                    }
                    
                    // 从队列中取出任务(移动语义避免不必要的拷贝)
                    task = std::move(m_tasks.front());
                    m_tasks.pop();
                }  // 锁的作用域结束,自动释放锁
                
                // 在锁外执行任务,避免任务执行期间阻塞其他线程
                task(); 
            }
        });
    }
} 

// 析构函数实现
inline ThreadPool::~ThreadPool() {
    {
        // 加锁设置停止标志
        std::unique_lock<std::mutex> lock(m_queueMutex);
        m_stop = true;
    }  // 锁的作用域结束,自动释放锁
    
    // 通知所有等待的线程检查停止标志
    m_cv.notify_all();
    
    // 等待所有工作线程完成当前任务并退出
    for (std::thread& thread : m_threads) {
        thread.join();
    }
}

// 任务提交函数模板实现
template<class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args) 
    -> std::future<typename std::result_of<F(Args...)>::type> {
    
    // 推导返回类型
    using return_type = typename std::result_of<F(Args...)>::type;
    
    // 创建packaged_task来包装用户任务,用于获取future
    // 使用shared_ptr使得任务对象可以在lambda中安全捕获
    auto task = std::make_shared<std::packaged_task<return_type()>>(
        std::bind(std::forward<F>(f), std::forward<Args>(args)...)
    );
    
    // 获取与任务关联的future,用于异步获取结果
    std::future<return_type> res = task->get_future();
    
    {
        // 加锁保护任务队列
        std::unique_lock<std::mutex> lock(m_queueMutex);
        
        // 检查线程池是否已停止
        if (m_stop) {
            throw std::runtime_error("enqueue on stopped ThreadPool");
        }
        
        // 将任务包装成void()类型并加入队列
        // 使用lambda捕获shared_ptr,确保任务对象生命周期
        m_tasks.emplace([task]() { 
            (*task)();  // 执行packaged_task
        });
    }  // 锁的作用域结束,自动释放锁
    
    // 通知一个等待的线程有新任务可用
    m_cv.notify_one();
    
    return res;  // 返回future给调用者
}

3.2 线程池的使用

cpp 复制代码
#include "ThreadPool.h"
#include <iostream>
#include <chrono>

// 示例函数
int add(int a, int b) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    return a + b;
}

int main() {
    // 1. 创建线程池(推荐使用硬件并发数)
    ThreadPool pool(std::thread::hardware_concurrency());
    
    // 2. 提交任务并获取future
    std::future<int> result1 = pool.enqueue(add, 10, 20);
    std::future<int> result2 = pool.enqueue([](int x) { return x * x; }, 5);
    
    // 3. 获取结果(会阻塞直到任务完成)
    std::cout << "Result1: " << result1.get() << std::endl;  // 输出 30
    std::cout << "Result2: " << result2.get() << std::endl;  // 输出 25
    
    return 0;
}

3.3 异常处理

cpp 复制代码
void mightThrow() {
    throw std::runtime_error("Task failed!");
}

int main() {
    ThreadPool pool(4);
    
    try {
        auto future = pool.enqueue(mightThrow);
        future.get();  // 这里阻塞,异常会在get()时重新抛出
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
    
    return 0;
}

3.4 批量放入任务

cpp 复制代码
void processBatchTasks() {
    ThreadPool pool(4);
    std::vector<std::future<int>> results;
    
    // 提交一批任务
    for (int i = 0; i < 10; ++i) {
        results.push_back(pool.enqueue([i] {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            return i * i;
        }));
    }
    
    // 等待所有任务完成并收集结果
    for (auto& result : results) {
        std::cout << result.get() << " ";
    }
    std::cout << std::endl;
}

3.5 任务为类成员函数

cpp 复制代码
class Calculator {
public:
    int multiply(int a, int b) {
        return a * b;
    }
    
    static int staticAdd(int a, int b) {
        return a + b;
    }
};

int main() {
    ThreadPool pool(4);
    Calculator calc;
    
    // 提交类成员函数(需要绑定对象实例)
    auto future1 = pool.enqueue(&Calculator::multiply, &calc, 6, 7);
    
    // 提交静态成员函数
    auto future2 = pool.enqueue(&Calculator::staticAdd, 3, 4);
    
    std::cout << "Multiply: " << future1.get() << std::endl;  // 输出 42
    std::cout << "Static Add: " << future2.get() << std::endl;  // 输出 7
    
    return 0;
}
相关推荐
一个天蝎座 白勺 程序猿2 小时前
KingbaseES约束机制:数据迁移中的数据完整性保障
开发语言·数据库·kingbase·kingbasees
仰泳的熊猫2 小时前
题目1474:蓝桥杯基础练习VIP-阶乘计算
数据结构·c++·算法·蓝桥杯
WBluuue2 小时前
数据结构与算法:dp优化——树状数组/线段树优化
数据结构·c++·算法·leetcode·动态规划
华科大胡子2 小时前
《Effective C++》学习笔记:条款02
c++·编程语言·inline·const·enum·define
tankeven2 小时前
HJ84 统计大写字母个数
c++·算法
~央千澈~2 小时前
抖音弹幕游戏开发之第9集:pyautogui进阶 - 模拟鼠标操作·优雅草云桧·卓伊凡
开发语言·python·游戏
遇见你的雩风2 小时前
【Golang】--- Goroutine
开发语言·golang
v沙加v2 小时前
Java Rendering Engine Unknown
java·开发语言
张3蜂2 小时前
python知识点点亮
开发语言·python