一、为什么需要多线程

1.1 单线程的瓶颈

假设你的电脑 CPU 有 4 个核心,但你的程序只用了一个核心。处理 10 张图片,每张 1 秒:

  • 单线程:10 秒

  • 4 个线程同时工作:理想情况 2.5 秒

多线程可以让程序同时使用多个 CPU 核心,大幅缩短批量任务的执行时间。如果你的程序需要同时处理很多互不依赖的任务(如批量下载、图像处理、数据转换),多线程几乎是必选项。


二、基础:手动创建线程

2.1 最简单的线程创建(正确版)

#include <iostream>
#include <thread>
#include <chrono>
#include <string>

using namespace std;

// 线程要执行的函数
void process_image(const string& img) {
    this_thread::sleep_for(chrono::seconds(1)); // 模拟耗时处理
    cout << img << " 处理完成" << endl;
}

int main() {
    // 创建两个线程,分别执行 process_image
    thread t1(process_image, "1.jpg");
    thread t2(process_image, "2.jpg");

    // 等待两个线程结束
    t1.join();
    t2.join();

    cout << "所有任务完成" << endl;
    return 0;
}

2.2 创建线程的语法

thread 线程对象(要执行的函数, 参数1, 参数2, ...);

  • 第一参数可以是普通函数、lambda 表达式、函数对象。

  • 后面的参数会原样传递给函数。

  • 线程对象一旦创建,线程立刻开始执行,不会等 join

2.3 join 是什么?为什么必须调用?

t.join() 的意思是:“主线程在此等待,直到线程 t 执行完再继续”

错误写法:忘记 join
int main() {
    thread t1(process_image, "1.jpg");
    // 忘记写 t1.join()
    cout << "主线程结束" << endl;
    return 0; 
} // t1 析构,但线程还在运行 -> 程序崩溃

后果:线程对象 t1 在 main 结束时析构,但此时它关联的线程仍在运行(或者已经结束但未被回收)。C++ 标准规定,如果线程对象在销毁时仍处于“joinable”状态(即未 join 也未 detach),就会调用 std::terminate(),导致整个程序强制崩溃
你可能会看到类似 terminate called without an active exception 的错误。

错误写法:忘记 join,还想用 detach 逃避
int main() {
    thread t1(process_image, "1.jpg");
    t1.detach(); // 分离线程
    return 0;
}

detach() 将线程与线程对象脱离关系,线程在后台独立运行。此时 main 退出后,后台线程可能还未执行完,这时它访问的局部变量(如果传了引用)会变成悬空引用,导致未定义行为。初学者一般不推荐使用 detach()


三、共享资源的线程安全问题

3.1 问题:cout 不是线程安全的

多个线程同时调用 cout 输出内容时,每个线程可能会在输出一半时被另一个线程打断,导致文字交错、乱序,甚至程序崩溃。

错误示例:无保护打印
void process_image(const string& img) {
    this_thread::sleep_for(chrono::seconds(1));
    cout << img << " 处理成功!" << endl; // 多线程同时写
}

运行结果可能出现

1.jpg 处理成功2.jpg!
 处理成功!

或者根本无法阅读的乱码。

为什么会这样?

cout 的底层输出是一个字节流,<< 操作是分多次写入的。线程 A 刚写了一半的字符串,操作系统切换到线程 B,线程 B 也写 cout,两者数据就混在一起了。这种数据竞争属于未定义行为

解决方案:加互斥锁(mutex)
#include <mutex>

mutex print_mtx; // 全局互斥锁,仅用于保护输出

void process_image(const string& img) {
    this_thread::sleep_for(chrono::seconds(1));
    {
        lock_guard<mutex> lock(print_mtx); // 获取锁
        // 在这个作用域内,其他线程无法同时获取 print_mtx
        cout << img << " 处理成功!" << endl;
    } // 离开作用域,锁自动释放
}

lock_guard 原理

  • 构造时自动调用 print_mtx.lock()

  • 析构时自动调用 print_mtx.unlock()

  • 即使中间抛出异常,也能正确解锁,不会死锁。

3.2 问题:多线程共享计数器

多个线程同时修改一个普通 int,会发生数据竞争。

错误示例:普通变量做计数器
int success_count = 0; // 普通变量

void process_image(const string& img) {
    this_thread::sleep_for(chrono::seconds(1));
    success_count++; // 多线程同时执行
}

后果:假设两个线程同时读到 success_count=0,各自加 1 后写回,最终结果可能仍是 1,丢失了一次增加。这就是经典的“丢失更新”问题。
而且,对普通 int 的并发读写属于未定义行为,编译器可能优化出完全错误的指令。

解决方案:使用原子变量 std::atomic
#include <atomic>

atomic<int> success_count{0};

void process_image(const string& img) {
    this_thread::sleep_for(chrono::seconds(1));
    success_count++; // 原子操作:要么完全执行,要么没执行
}

atomic<int> 保证 ++ 操作不会被其他线程打断,无数据竞争。

3.3 mutex vs atomic 如何选择?

场景 推荐 原因
单个整数/指针/布尔值的读写、自增 std::atomic 硬件直接支持,极快,无锁
修改多个变量(需要保持一致性) std::mutex 必须保证“同时修改”这些变量时不被中断
操作 STL 容器(如 vectormap std::mutex 容器本身不是线程安全的,必须加锁
需要“读-修改-写”复杂逻辑 std::mutex 原子操作只能完成非常简单的步骤

四、进阶:生产者 - 消费者模型(共享队列抢任务)

4.1 为什么需要任务队列?

手动给每个线程分配任务(比如 t1 处理前 5 张,t2 处理后 5 张)太僵硬:

  • 如果有的图片处理快、有的慢,先完成的线程会闲着,后完成的还在忙,负载不均衡。

  • 任务数量变化时很难分配。

更好的方式:所有任务丢到一个公共队列,多个线程自动从队列里取任务。谁空闲谁就去取下一个,自动均衡。

4.2 模型组件

  • 生产者:负责添加任务(主线程)。

  • 消费者:负责取任务并执行(工作线程)。

  • 共享队列:存储任务。

  • 同步机制:互斥锁保护队列,条件变量用于等待/通知。

4.3 初学者的典型错误:忙等待

错误写法:工作线程里循环检测队列
void worker() {
    while (!done) {
        if (!task_queue.empty()) { // 无锁检查,错误!
            // 取任务...
        }
    }
}

同时主线程可能这样检查任务是否全部完成:

while (true) {
    if (task_queue.empty()) break;
}

问题

  • 不加锁读取队列 -> 数据竞争 -> 未定义行为。

  • 即使加了锁,这种循环也会让 CPU 一个核跑到 100%,空耗电力,还影响其他线程的性能。这叫做忙等待(busy waiting)

✅ 正确方式:使用条件变量 std::condition_variable

条件变量能让线程“睡”在某个条件上,直到另一个线程通知它“条件可能满足了”,再醒来检查。

4.4 完整正确实现(带详细注释)

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <string>
#include <atomic>
#include <chrono>
#include <random>

using namespace std;

// 共享资源
queue<string> task_queue;       // 任务队列
mutex queue_mtx;                // 保护队列的锁
mutex print_mtx;                // 保护输出的锁(分离锁,减少竞争)
condition_variable cv;          // 条件变量(用于“有任务”和“任务空”的通知)
atomic<bool> done{false};       // 生产者是否不再添加任务

atomic<int> success_count{0};
atomic<int> fail_count{0};

void worker() {
    while (true) {
        string img;
        // ----- 取任务(加锁保护)-----
        {
            // 这里必须用 unique_lock,因为条件变量需要手动解锁
            unique_lock<mutex> lock(queue_mtx);
            
            // 等待条件:队列不空 OR 生产者结束
            // 如果条件不满足,线程会解锁并进入睡眠,不消耗 CPU
            cv.wait(lock, []{ return !task_queue.empty() || done; });
            
            // 被唤醒后,检查退出条件
            if (task_queue.empty() && done) {
                break;  // 队列空且不再有新任务,线程退出
            }
            
            img = task_queue.front();
            task_queue.pop();
        } // 锁在这里自动释放
        
        // ----- 处理任务(无锁)-----
        this_thread::sleep_for(chrono::seconds(1));
        
        // 线程局部随机数(每个线程只初始化一次,高效)
        thread_local mt19937 gen(random_device{}());
        uniform_int_distribution<int> dist(1, 10);
        int r = dist(gen);
        
        if (r < 9) success_count++;
        else fail_count++;
        
        // ----- 打印结果(使用输出锁)-----
        {
            lock_guard<mutex> lock(print_mtx);
            cout << img << (r < 9 ? " 处理成功!" : " 处理失败!") << endl;
        }
        
        // ----- 通知主线程:队列可能已空(可选的优化)-----
        {
            lock_guard<mutex> lock(queue_mtx);
            if (task_queue.empty()) {
                cv.notify_one(); // 通知等待“队列空”的主线程
            }
        }
    }
}

int main() {
    // 1. 生产任务
    for (int i = 1; i <= 10; ++i) {
        task_queue.push(to_string(i) + ".jpg");
    }

    // 2. 启动消费者
    thread t1(worker);
    thread t2(worker);

    // 3. 主线程等待队列变空(优雅等待,非忙等)
    {
        unique_lock<mutex> lock(queue_mtx);
        cv.wait(lock, []{ return task_queue.empty(); });
    }

    // 4. 通知所有消费者可以退出了
    done = true;
    cv.notify_all();  // 唤醒可能在睡眠的线程,让他们检查 done

    // 5. 等待线程回收
    t1.join();
    t2.join();

    // 6. 此时所有线程已结束,可以安全读取统计结果
    cout << "\n===== 全部处理完成 =====" << endl;
    cout << "成功:" << success_count << " 张" << endl;
    cout << "失败:" << fail_count << " 张" << endl;

    return 0;
}

4.5 condition_variable 深入理解

cv.wait(lock, 条件函数) 做了什么?

  1. 检查条件函数,如果返回 true,立刻返回(不睡眠)。

  2. 如果返回 false,解锁 lock,然后阻塞当前线程(进入睡眠,不耗 CPU)。

  3. 当其他线程调用 cv.notify_one() 或 notify_all() 时,睡眠线程被唤醒。

  4. 醒来后重新获取锁,再次检查条件函数,如果是 true 则返回;否则继续睡。

为什么传 unique_lock 而不是 lock_guard
condition_variable 需要在 wait 时解锁、醒来后重新加锁,而 lock_guard 不允许手动解锁,所以必须用更灵活的 unique_lock

4.6 初学者最容易忽略的一个后果

忘记 done 标志或判断错误
如果主线程不再添加任务,但没有设置 done=true,且队列空了,工作线程会永远睡在 cv.wait 上,导致 join() 永远无法返回,程序死锁


五、终极:线程池的实现与使用

5.1 为什么需要线程池?

手动创建线程有以下问题:

  • 创建/销毁开销大:线程的创建涉及系统调用,频繁创建会拖慢程序。

  • 数量不可控:如果无限制地开新线程,线程过多会导致操作系统调度开销剧增,甚至内存耗尽。

  • 代码重复:每次都要写 threadjoin、任务队列、条件变量,容易出错。

线程池就是提前创建好一定数量的线程,让它们复用:线程始终运行,不停地从任务队列取任务执行。你只需要往池子里“扔”任务即可。

5.2 常见错误:误把“固定分配”当成线程池

网上有些教程(甚至旧版本文)可能会写出这样的“线程池”:

// 错误!这不是线程池,只是为每个任务分配一个线程
class FakeThreadPool {
public:
    FakeThreadPool(size_t num_threads, const vector<Task>& tasks) {
        for (size_t i = 0; i < num_threads; ++i) {
            workers.emplace_back(tasks[i]); // 只执行 tasks[i]
        }
    }
    ...
};

致命缺陷

  • 要求任务数 ≥ 线程数,否则数组越界崩溃

  • 只能执行与线程数相等的任务,多余的任务根本不被执行。

  • 线程没有复用,每个线程只执行一个任务就结束了,退化为手动创建线程。

5.3 正确线程池的核心设计

一个真正的线程池必须具备:

  1. 任务队列(队列可动态添加任务)。

  2. 一组预先创建的工作线程,它们在循环中不断从队列取任务。

  3. 提交任务的接口enqueue 或 submit)。

  4. 同步机制:互斥锁+条件变量,用于安全地添加/取出任务。

  5. 优雅退出:析构时通知所有线程结束,并等待它们完成剩余任务。

5.4 正确且极简的线程池实现(可直接保存为 thread_pool.h

#ifndef THREAD_POOL_H
#define THREAD_POOL_H

#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>

class ThreadPool {
public:
    using Task = std::function<void()>;  // 可存放任何可调用对象

    // 构造函数:启动 num_threads 个工作线程
    explicit ThreadPool(size_t num_threads) : stop(false) {
        for (size_t i = 0; i < num_threads; ++i) {
            workers.emplace_back([this] {
                // 线程主循环
                while (true) {
                    Task task;
                    {
                        // 锁定队列,等待任务或停止信号
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        condition.wait(lock, [this] {
                            return stop || !tasks.empty();
                        });
                        // 如果线程池要求停止且队列已空,退出线程
                        if (stop && tasks.empty()) return;
                        
                        // 取出任务
                        task = std::move(tasks.front());
                        tasks.pop();
                    } // 解锁
                    
                    // 执行任务(无锁状态,不影响其他线程取任务)
                    task();
                }
            });
        }
    }

    // 提交任务(任何可调用对象:函数、lambda 等)
    template<typename F>
    void enqueue(F&& f) {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            // 如果已经停止,通常不应该再添加任务,这里简单起见不抛出异常
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one(); // 唤醒一个等待的线程
    }

    // 析构函数:停止所有线程,等待完成
    ~ThreadPool() {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();          // 唤醒所有线程
        for (std::thread& worker : workers) {
            if (worker.joinable()) {
                worker.join();           // 等待线程退出
            }
        }
    }

    // 禁止拷贝
    ThreadPool(const ThreadPool&) = delete;
    ThreadPool& operator=(const ThreadPool&) = delete;

private:
    std::vector<std::thread> workers;          // 工作线程数组
    std::queue<Task> tasks;                    // 任务队列
    std::mutex queue_mutex;                     // 保护队列的锁
    std::condition_variable condition;          // 条件变量
    bool stop;                                  // 停止标志
};

#endif

5.5 线程池使用示例

#include <iostream>
#include <vector>
#include <string>
#include <mutex>
#include <atomic>
#include <chrono>
#include <random>
#include "thread_pool.h"

using namespace std;

// 保护输出的锁
mutex print_mtx;
atomic<int> success_count{0};
atomic<int> fail_count{0};

void process_image(const string& img) {
    this_thread::sleep_for(chrono::seconds(1)); // 模拟工作

    // 线程局部随机数生成器(避免每次构造)
    thread_local mt19937 gen(random_device{}());
    uniform_int_distribution<int> dist(1, 10);
    int r = dist(gen);

    if (r < 9) success_count++;
    else fail_count++;

    lock_guard<mutex> lock(print_mtx);
    cout << img << (r < 9 ? " 处理成功!" : " 处理失败!") << endl;
}

int main() {
    // 准备任务数据
    vector<string> images;
    for (int i = 1; i <= 10; ++i) {
        images.push_back(to_string(i) + ".jpg");
    }

    // 创建线程池(2 个常驻线程)
    ThreadPool pool(2);

    // 提交所有任务
    for (const auto& img : images) {
        pool.enqueue([img]() {
            process_image(img);
        });
    }

    // 此处 pool 离开作用域时:
    // 1. 析构函数设置 stop=true,并唤醒所有线程
    // 2. 工作线程执行完剩余任务后退出循环
    // 3. 主线程 join 所有工作线程后,析构结束
    // 所以下面输出时,所有任务必然已全部完成

    cout << "\n===== 全部处理完成 =====" << endl;
    cout << "成功:" << success_count << " 张" << endl;
    cout << "失败:" << fail_count << " 张" << endl;

    return 0;
}

5.6 为什么析构函数能保证任务全部完成?

析构函数执行顺序:

  1. 获取 queue_mutex,设置 stop = true,释放锁。

  2. condition.notify_all() 唤醒所有可能在等待任务的工作线程。

  3. for 循环依次对每个工作线程调用 join()

工作线程被唤醒后,检查 stop && tasks.empty(),如果此时队列里还有任务,条件为 false,线程会继续取任务执行,直到队列真正为空,下一次循环再检查才会退出。主线程在 join 处等待,所以所有任务一定会在析构函数返回前执行完毕。

常见误区:如果你在创建线程池后,立刻又在主线程里打印统计结果,而没有等池子析构,就可能读到不完整的数值。所以如果不想依赖析构等待,可以手动调用一个 wait() 方法,但此处利用析构是最简单可靠的方式。


六、常见坑点总结及后果分析

错误行为 具体后果 正确做法
忘记 join() 或 detach() 程序调用 std::terminate() 崩溃 务必 join() 或 detach(),初学者优先 join()
在多线程中直接使用普通变量共享 数据竞争(丢失更新、读出脏数据),未定义行为,难以调试 std::atomic 或加锁
多个线程同时 cout,不加锁 输出乱序、交叠、崩溃 用独立的 std::mutex 保护输出
操作共享容器(队列、vector等)不加锁 容器内部结构破坏,程序可能 segment fault 所有读写容器操作必须在锁内完成
使用忙等待循环检测条件 CPU 100% 空转,耗电,影响其他线程性能 使用 std::condition_variable 等待/通知
条件变量忘记设置 done 或退出条件 消费者永久睡眠,join() 永远等待,程序死锁 确保有一个“不再生产”的标志,并正确判断
在任务未完成时访问结果 读到不完整/错误的数据 确保所有线程 join 后,或析构线程池后再读取
随机数生成器每次重新构造 性能极差,且可能产生相同随机序列 使用 thread_local 静态生成器
锁粒度过大(例如输出和队列用同一把锁) 不必要的等待,降低并发效率 无关资源用不同的锁

七、最终通用模板(可直接复制使用)

thread_pool.h


#ifndef THREAD_POOL_H
#define THREAD_POOL_H
#include <vector>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <functional>

class ThreadPool {
public:
    using Task = std::function<void()>;

    explicit ThreadPool(size_t num_threads) : stop(false) {
        for (size_t i = 0; i < num_threads; ++i)
            workers.emplace_back([this] {
                while (true) {
                    Task task;
                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        condition.wait(lock, [this] { return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) return;
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    task();
                }
            });
    }

    template<typename F>
    void enqueue(F&& f) {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            tasks.emplace(std::forward<F>(f));
        }
        condition.notify_one();
    }

    ~ThreadPool() {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            stop = true;
        }
        condition.notify_all();
        for (std::thread& w : workers)
            if (w.joinable()) w.join();
    }

private:
    std::vector<std::thread> workers;
    std::queue<Task> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    bool stop;
};
#endif

使用模板(三步走)

#include "thread_pool.h"
#include <vector>
#include <iostream>

// 1. 写你的业务函数
void your_function(const YourParam& param) {
    // 你的处理逻辑
}

int main() {
    std::vector<YourParam> data;
    // 填充数据 ...

    // 2. 创建线程池(比如 4 个线程)
    ThreadPool pool(4);

    // 3. 提交任务
    for (const auto& d : data) {
        pool.enqueue([d]() {
            your_function(d);
        });
    }
    // 4. 离开作用域,析构自动等待所有任务完成
    return 0;
}

八、总结

我们一步步解决了:

  • 线程的创建与等待,以及忘记 join 的崩溃隐患。

  • cout 乱序 和 计数器丢失 的原因与修复(锁与原子)。

  • 生产者-消费者模型的高效实现,消除了忙等待,学会了条件变量的使用。

  • 线程池的真正含义,并通过一个短小但完整正确的实现,展示了如何复用线程。

这个线程池虽“极简”,但包含了任务队列、线程复用、同步通知、优雅退出等工业级基础。充分理解之后,可以根据需求添加功能(如返回值的 std::future、任务优先级等)。

更多推荐