C++ thread 详解,附上代码示例

概念
C++ 里的线程,通常指一个进程内可以并发执行的独立执行流。标准库从 C++11 开始提供线程支持,核心类是 std::thread。你可以把它理解成:主线程之外,再开几个“工人”同时做事,比如并行计算、后台日志、网络收发、任务调度。

但线程不是“免费提速”。它会带来几个核心问题:

  1. 线程如何创建和回收
  2. 多个线程同时访问同一份数据时,如何避免数据竞争
  3. 线程之间如何通信和等待
  4. 怎么优雅停止线程,避免资源泄漏或程序崩溃

下面按这条线讲。


一、最基础:创建线程

最常见的写法是把一个函数交给 std::thread 执行。

#include <iostream>
#include <thread>

void worker() {
    std::cout << "worker thread id = " << std::this_thread::get_id() << '\n';
}

int main() {
    std::thread t(worker);

    std::cout << "main thread id = " << std::this_thread::get_id() << '\n';

    t.join(); // 等待子线程执行完成
    return 0;
}

这里有几个关键点:

  1. std::thread t(worker); 会立刻启动一个新线程。
  2. t.join(); 表示主线程等待 t 执行完。
  3. 如果一个 std::thread 对象在析构前既没有 join,也没有 detach,程序会直接 terminate。

所以这段代码里,join 不是“建议”,而是必须处理。


二、join 和 detach 的区别

线程启动后,必须在这两个里选一个:

  1. join
    当前线程等待目标线程结束
  2. detach
    目标线程脱离管理,后台独立运行

示例:

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

void task() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "task finished\n";
}

int main() {
    std::thread t(task);

    // 方案1:等待结束
    t.join();

    // 方案2:后台运行
    // t.detach();

    return 0;
}

detach 要慎用。因为一旦 detach:

  1. 你无法再 join 它
  2. 很难控制它的生命周期
  3. 如果它访问了已经销毁的对象,容易出严重 bug

实际开发里,大多数情况优先用 join,或者用 C++20 的 std::jthread。


三、线程传参

std::thread 默认会把参数按值拷贝到新线程里。

#include <iostream>
#include <thread>

void printValue(int x) {
    std::cout << "x = " << x << '\n';
}

int main() {
    int a = 10;
    std::thread t(printValue, a);
    t.join();
    return 0;
}

如果你想传引用,必须显式用 std::ref。

#include <iostream>
#include <thread>
#include <functional>

void addOne(int& x) {
    ++x;
}

int main() {
    int a = 10;
    std::thread t(addOne, std::ref(a));
    t.join();

    std::cout << "a = " << a << '\n'; // 11
    return 0;
}

如果不加 std::ref,线程拿到的是副本,不会改到原变量。

也可以直接用 lambda:

#include <iostream>
#include <thread>

int main() {
    int a = 42;

    std::thread t([a]() {
        std::cout << "a = " << a << '\n';
    });

    t.join();
    return 0;
}

如果 lambda 用引用捕获,也要保证被引用对象在线程执行期间仍然有效。


四、最常见问题:数据竞争

多个线程同时读写同一份数据,如果没有同步,就是数据竞争。结果是未定义行为。

错误示例:

#include <iostream>
#include <thread>

int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "counter = " << counter << '\n';
    return 0;
}

你可能以为结果一定是 200000,但实际经常不是。因为 ++counter 不是原子操作,它通常会分成读、改、写三个步骤,两个线程会互相踩。


五、用 mutex 保护共享数据

解决共享数据竞争,最常用的是互斥锁 std::mutex。

#include <iostream>
#include <thread>
#include <mutex>

int counter = 0;
std::mutex mtx;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "counter = " << counter << '\n';
    return 0;
}

这里推荐用 std::lock_guard,而不是手动调用 mtx.lock() / mtx.unlock()。原因很简单:

  1. 自动释放锁
  2. 遇到异常也不会忘记 unlock
  3. 写法更安全

等价但不推荐的写法是:

mtx.lock();
++counter;
mtx.unlock();

这种方式一旦中途 return 或抛异常,就容易死锁。


六、什么时候用 atomic 而不是 mutex

如果只是做简单计数、自增、自减、状态位修改,可以用 std::atomic,通常比 mutex 更轻量。

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter{0};

void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "counter = " << counter << '\n';
    return 0;
}

选择原则很简单:

  1. 简单单变量同步,用 atomic
  2. 复杂临界区、多个变量一致性,用 mutex

atomic 不是 mutex 的全替代品。


七、线程等待条件:condition_variable

如果一个线程要“等某个条件成立再继续”,不要用死循环一直检查,这会浪费 CPU。应该用条件变量。

典型场景:生产者消费者。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
using namespace std;

std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
bool finished = false;

void producer() {
    for (int i = 1; i <= 10000; ++i) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            q.push(i);
            cout << "producer " << i << endl;
        }
        cv.notify_one();
        cout << "producer notify_one: " << i << endl;
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        finished = true;
        cout << "producer finished\n";
    }
    cv.notify_one();
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] {
            cout << "consume wait" << endl;
            return !q.empty() || finished;
            });

        while (!q.empty()) {
            int value = q.front();
            q.pop();
            cout << "consume: " << value << ", left: " << q.size() << endl;
        }

        if (finished) {
            cout << "consumer finished\n";
            break;
        }
    } // end while
}

int main()
{
    std::thread t1(producer);
    std::thread t2(consumer);

    t1.join();
    t2.join();

    return 0;
}

这里要注意两点:

  1. wait 最好带条件谓词
  2. 不能假设被唤醒后条件一定满足,因为可能有“虚假唤醒”

所以推荐这种形式:

cv.wait(lock, [] { return 条件成立; });

而不是只写:

cv.wait(lock);


八、std::lock_guard 和 std::unique_lock 的区别

两者都能管理锁,但定位不同:

  1. std::lock_guard
    最轻量,作用域内自动加锁解锁
  2. std::unique_lock
    更灵活,可以延迟加锁、手动解锁、配合 condition_variable 使用

示例:

std::mutex mtx;

void f1() {
    std::lock_guard<std::mutex> lock(mtx);
    // 临界区
}

void f2() {
    std::unique_lock<std::mutex> lock(mtx);
    // 临界区
    lock.unlock();
    // 非临界区
}

一般规则:

  1. 简单加锁,优先 lock_guard
  2. 需要和条件变量配合,或者需要手动 unlock,再用 unique_lock

九、成员函数作为线程入口

线程函数也可以是类成员函数,但需要额外传 this。

#include <iostream>
#include <thread>

class Task {
public:
    void run(int x) {
        std::cout << "run: " << x << '\n';
    }
};

int main() {
    Task task;
    std::thread t(&Task::run, &task, 123);
    t.join();
    return 0;
}

格式是:

  1. 成员函数指针
  2. 对象地址或对象引用包装
  3. 成员函数参数

十、C++20 更推荐:std::jthread

如果编译器支持 C++20,很多时候 std::jthread 比 std::thread 更好用,因为它析构时会自动 join,不容易忘。

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

void worker() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "worker done\n";
}

int main() {
    std::jthread t(worker);
    std::cout << "main end\n";
    return 0; // t 析构时自动 join
}

它还支持停止请求 stop_token,这是现代 C++ 做线程取消的推荐方式。

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

void worker(std::stop_token st) {
    while (!st.stop_requested()) {
        std::cout << "working...\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
    }
    std::cout << "stopped\n";
}

int main() {
    std::jthread t(worker);

    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();

    return 0;
}

如果你在写新项目,优先考虑 std::jthread。


十一、线程池和 std::thread 的关系

std::thread 是“直接创建一个系统线程”。如果任务很多,频繁创建销毁线程成本会比较高。这时更常见的工程方案是线程池:

  1. 预先创建固定数量线程
  2. 任务来了放进队列
  3. 工作者线程不断从队列取任务执行

标准库本身没有正式线程池类型,所以企业项目里通常会自己实现或用第三方库。也就是说:

  1. 少量线程控制,用 std::thread
  2. 大量小任务调度,用线程池更合适

十二、常见坑

1. 忘记 join 或 detach

std::thread t(worker);
// 函数结束直接退出,t 析构时 terminate

这是最常见错误之一。

2. detach 后访问悬空对象

void f() {
    int x = 10;
    std::thread([&]() {
        std::cout << x << '\n';
    }).detach();
}

这里线程可能在 f 返回后才执行,x 已经销毁,属于悬空引用。

3. 多线程输出混乱

多个线程同时往 std::cout 写,输出可能交错。解决方式通常是:

  1. 给输出加锁
  2. 先拼接到字符串,再统一输出
  3. 用线程安全日志库

4. 死锁

例如两个线程以不同顺序拿两把锁:

// 线程1: 先锁 A,再锁 B
// 线程2: 先锁 B,再锁 A

这非常容易死锁。常见规避方法:

  1. 统一加锁顺序
  2. 用 std::scoped_lock 同时锁多个 mutex

示例:

#include <mutex>

std::mutex m1, m2;

void safe() {
    std::scoped_lock lock(m1, m2);
    // 同时安全锁住两把锁
}

十三、一个稍完整的例子:多线程处理任务

下面这个例子演示两个工作线程,从任务队列中取数据并处理。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <chrono>

class TaskQueue {
public:
    void push(int task) {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            tasks_.push(task);
        }
        cv_.notify_one();
    }

    bool pop(int& task) {
        std::unique_lock<std::mutex> lock(mtx_);
        cv_.wait(lock, [this] {
            return !tasks_.empty() || stopped_;
        });

        if (tasks_.empty()) {
            return false;
        }

        task = tasks_.front();
        tasks_.pop();
        return true;
    }

    void stop() {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            stopped_ = true;
        }
        cv_.notify_all();
    }

private:
    std::queue<int> tasks_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool stopped_ = false;
};

void worker(TaskQueue& queue, int workerId) {
    int task;
    while (queue.pop(task)) {
        std::cout << "worker " << workerId
                  << " processing task " << task << '\n';
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }
    std::cout << "worker " << workerId << " exit\n";
}

int main() {
    TaskQueue queue;

    std::vector<std::thread> workers;
    for (int i = 0; i < 2; ++i) {
        workers.emplace_back(worker, std::ref(queue), i + 1);
    }

    for (int i = 1; i <= 10; ++i) {
        queue.push(i);
    }

    std::this_thread::sleep_for(std::chrono::seconds(1));
    queue.stop();

    for (auto& t : workers) {
        t.join();
    }

    return 0;
}

这个例子里包含了线程编程的几个核心要素:

  1. 多线程并发执行
  2. 共享任务队列
  3. mutex 保护共享数据
  4. condition_variable 阻塞等待任务
  5. stop 信号控制退出
  6. join 回收线程

这已经是很多服务端程序的基本模型了。


十四、怎么理解线程安全

“线程安全”不是一句空话,它通常意味着:

  1. 多个线程同时调用同一段代码,不会破坏数据一致性
  2. 不会出现未定义行为
  3. 结果符合预期

比如下面这个类不是线程安全的:

class Counter {
public:
    void increment() {
        ++value_;
    }

    int get() const {
        return value_;
    }

private:
    int value_ = 0;
};

因为多个线程同时 increment 会竞争。改成这样才更安全:

#include <mutex>

class Counter {
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx_);
        ++value_;
    }

    int get() const {
        std::lock_guard<std::mutex> lock(mtx_);
        return value_;
    }

private:
    mutable std::mutex mtx_;
    int value_ = 0;
};

十五、学习线程时的建议路线

建议按这个顺序掌握,不要一上来就碰复杂并发模型:

  1. 先会用 std::thread 创建线程
  2. 理解 join 和 detach
  3. 理解共享数据和数据竞争
  4. 学会 mutex、lock_guard、unique_lock
  5. 学会 condition_variable
  6. 再去看 atomic、future、async、jthread、线程池

如果前面没吃透,后面基本都是“看起来懂了,实际写就出 bug”。


十六、编译说明

如果你用 GCC 或 Clang,通常需要开启 C++11 及以上,并加线程选项:

g++ main.cpp -std=c++17 -pthread

如果你用 MSVC,通常这样即可:

cl /EHsc /std:c++17 main.cpp

如果用到 std::jthread,需要 C++20:

g++ main.cpp -std=c++20 -pthread

    总结
    C++ 线程的核心不是“怎么开线程”,而是“怎么正确管理并发”。真正要掌握的是这几个点:

    1. 线程生命周期:创建、join、停止
    2. 共享数据同步:mutex、atomic
    3. 线程通信:condition_variable
    4. 资源安全:RAII,避免悬空引用和死锁
    5. 新代码优先考虑 C++20 的 std::jthread

    十七、C++ thread 面试题

    1. std::thread 和进程的区别是什么
      线程是进程内的执行流,同一进程下的线程共享地址空间和大多数资源;进程之间默认隔离更强,切换和通信成本通常更高。

    2. std::thread 创建后为什么必须 join 或 detach
      因为线程对象析构时如果仍然处于 joinable 状态,程序会直接调用 std::terminate。也就是说线程生命周期必须显式处理。

    3. join 和 detach 的区别
      join 是等待线程结束,并回收线程相关资源。
      detach 是让线程独立运行,当前对象不再管理它。
      工程里通常优先 join,因为 detach 很容易引入悬空引用和失控生命周期问题。

    4. 什么是数据竞争
      多个线程在没有同步的情况下,同时访问同一块内存,并且至少有一个是写操作,这就是数据竞争,结果属于未定义行为。

    5. mutex 和 atomic 怎么选
      atomic 适合单个变量的简单原子读写、自增、自减、状态位。
      mutex 适合保护一段临界区,或者多个变量的一致性。
      一句话:简单共享状态用 atomic,复杂共享状态用 mutex。

    6. lock_guard 和 unique_lock 的区别
      lock_guard 更轻量,只负责作用域加锁解锁。
      unique_lock 更灵活,可以延迟加锁、提前解锁、转移所有权,还能配合 condition_variable 使用。

    7. condition_variable 的作用
      让线程在条件不满足时睡眠等待,而不是忙等。适合生产者消费者、任务队列、事件通知等场景。

    8. 为什么 wait 一般要配合谓词使用
      因为存在虚假唤醒。正确写法是“醒来以后重新检查条件”,标准库已经把这个模式封装在 wait(lock, predicate) 里了。

    9. 什么是死锁,怎么避免
      两个或多个线程彼此等待对方释放锁,导致永久阻塞。
      常见避免方式:

    10. 统一加锁顺序

    11. 一次性锁多把锁,比如 scoped_lock

    12. 缩小临界区

    13. 尽量避免锁嵌套

    14. 线程池为什么比频繁 new thread 更合适
      频繁创建销毁线程成本高。线程池通过复用固定数量的工作线程,从任务队列取任务执行,能显著降低调度开销,适合大量短任务。

    15. future、promise、packaged_task 分别是什么
      future:拿结果
      promise:设置结果
      packaged_task:把“可调用对象”包装成可异步执行并可通过 future 取结果的任务

    16. C++20 的 jthread 比 thread 好在哪里
      jthread 析构时自动 join,而且支持 stop_token,更适合现代 C++ 的可控退出模型。

    17. 下面代码有没有问题
      int x = 0;
      std::thread t(& { ++x; });
      t.detach();

    有风险。因为如果外层作用域结束,x 被销毁,而后台线程还没执行完,就会访问悬空引用。

    1. 多线程下 cout 为什么会乱
      因为多个线程同时输出时,字符流可能交叉写入。解决方式通常是给输出加锁,或者使用线程安全日志系统。

    2. 什么叫线程安全
      多个线程并发调用时,不会发生数据竞争,不会破坏对象状态,结果仍然正确。

    十八、简易线程池完整代码
    下面这个版本基于 C++17,可直接提交任务,并拿到 future 结果。

    #include <condition_variable>
    #include <functional>
    #include <future>
    #include <iostream>
    #include <mutex>
    #include <queue>
    #include <stdexcept>
    #include <thread>
    #include <type_traits>
    #include <utility>
    #include <vector>
    #include <chrono>
    
    class ThreadPool {
    public:
        explicit ThreadPool(std::size_t threadCount) : stop_(false) {
            if (threadCount == 0) {
                throw std::invalid_argument("threadCount must be greater than 0");
            }
    
            workers_.reserve(threadCount);
            for (std::size_t i = 0; i < threadCount; ++i) {
                workers_.emplace_back([this] {
                    for (;;) {
                        std::function<void()> task;
    
                        {
                            std::unique_lock<std::mutex> lock(mutex_);
                            condition_.wait(lock, [this] {
                                return stop_ || !tasks_.empty();
                            });
    
                            if (stop_ && tasks_.empty()) {
                                return;
                            }
    
                            task = std::move(tasks_.front());
                            tasks_.pop();
                        }
    
                        task();
                    }
                });
            }
        }
    
        ThreadPool(const ThreadPool&) = delete;
        ThreadPool& operator=(const ThreadPool&) = delete;
    
        ~ThreadPool() {
            {
                std::lock_guard<std::mutex> lock(mutex_);
                stop_ = true;
            }
    
            condition_.notify_all();
    
            for (std::thread& worker : workers_) {
                if (worker.joinable()) {
                    worker.join();
                }
            }
        }
    
        template <class F, class... Args>
        auto enqueue(F&& f, Args&&... args)
            -> std::future<std::invoke_result_t<F, Args...>> {
            using ReturnType = std::invoke_result_t<F, Args...>;
    
            auto taskPtr = std::make_shared<std::packaged_task<ReturnType()>>(
                std::bind(std::forward<F>(f), std::forward<Args>(args)...)
            );
    
            std::future<ReturnType> result = taskPtr->get_future();
    
            {
                std::lock_guard<std::mutex> lock(mutex_);
                if (stop_) {
                    throw std::runtime_error("enqueue on stopped ThreadPool");
                }
    
                tasks_.emplace([taskPtr] {
                    (*taskPtr)();
                });
            }
    
            condition_.notify_one();
            return result;
        }
    
    private:
        std::vector<std::thread> workers_;
        std::queue<std::function<void()>> tasks_;
    
        std::mutex mutex_;
        std::condition_variable condition_;
        bool stop_;
    };
    
    int slowSquare(int x) {
        std::this_thread::sleep_for(std::chrono::milliseconds(300));
        return x * x;
    }
    
    int main() {
        ThreadPool pool(4);
    
        std::vector<std::future<int>> results;
        for (int i = 1; i <= 8; ++i) {
            results.push_back(pool.enqueue(slowSquare, i));
        }
    
        for (std::size_t i = 0; i < results.size(); ++i) {
            std::cout << "task " << i + 1 << " result = "
                      << results[i].get() << '\n';
        }
    
        auto sumFuture = pool.enqueue([](int a, int b) {
            return a + b;
        }, 10, 20);
    
        std::cout << "sum = " << sumFuture.get() << '\n';
        return 0;
    }

    这份线程池怎么工作
    先看整体结构:

    1. workers_
      保存一组工作线程。

    2. tasks_
      任务队列,里面每个元素都是一个无参函数对象,也就是 std::function<void()>。

    3. mutex_ + condition_
      保护任务队列,并让工作线程在“没有任务”时休眠等待。

    4. stop_
      线程池停止标志。析构时会把它设为 true,然后唤醒所有工作线程退出。

    工作线程的核心逻辑是一个死循环:

    1. 先拿锁
    2. 如果任务队列为空,就用 condition_variable 等待
    3. 醒来后,如果 stop_ 为 true 且任务也空了,就退出线程
    4. 否则取出一个任务执行
    5. 回到循环继续等下一个任务

    也就是说,线程池的本质就是:

    1. 固定数量线程常驻
    2. 任务不断入队
    3. 空闲线程不断出队执行

    为什么 enqueue 返回 future
    因为很多时候你不只是想“扔个任务过去”,你还想拿它的返回值。

    例如:

    auto future = pool.enqueue([] {
        return 42;
    });
    
    int answer = future.get();

    这里的关键是 std::packaged_task:

    1. 它把一个函数包装成“将来可拿结果”的任务
    2. get_future 可以取到对应 future
    3. 任务真正执行后,返回值会自动写入 future

    所以线程池里做的是:

    1. 把用户提交的函数和参数绑定起来
    2. 包装成 packaged_task
    3. 再把这个 packaged_task 放进任务队列
    4. 工作线程执行它
    5. 用户通过 future.get() 取结果

    几个容易问到的实现细节

    1. 为什么任务队列存的是 std::function<void()>
      因为这样最统一。无论原始任务有没有参数、有没有返回值,最后都可以包装成“一个无参可执行单元”塞进队列。

    2. 为什么取任务时要先解锁再执行
      因为任务执行可能很慢。如果一直持有锁,别的线程就无法入队或出队,整个线程池几乎退化成串行。

    3. 为什么析构时先设 stop_,再 notify_all
      因为要让所有等待中的线程都醒来,检查退出条件并结束。

    4. 为什么 stop_ 和 tasks_ 的判断要放在同一个锁保护下
      否则会发生竞态,可能导致线程漏掉任务或者错误退出。

    这个实现的边界
    这个版本适合学习和中小型使用,但还不是工业级完整版本。它还没有覆盖这些高级能力:

    1. 任务优先级
    2. 动态扩缩容
    3. 有界队列和拒绝策略
    4. 取消任务
    5. 工作窃取
    6. 统计监控
    7. 异常上报策略

    不过作为面试题和基础实现,这个版本已经足够标准。

    面试里怎么讲线程池
    你可以用这套表达,比较稳:

    线程池本质上是“线程复用 + 任务队列”。
    核心目标是避免频繁创建销毁线程的开销。
    实现上一般包含三部分:工作线程集合、任务队列、同步原语。
    工作线程不断从队列中取任务执行;队列为空时通过条件变量阻塞等待;线程池销毁时通过停止标志和通知机制让所有线程有序退出。
    如果需要返回值,通常结合 packaged_task 和 future 实现。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    宏笋

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值