QuanZhou's Wiki

C++ 线程库

~/ C/C++#C++特性

C++ 线程库解决什么问题?

C++11 开始,标准库正式引入多线程支持,主要提供:

#include <thread>
#include <mutex>
#include <shared_mutex>
#include <condition_variable>
#include <future>
#include <atomic>

它解决的问题包括:

1. 创建和管理线程
2. 保护共享数据
3. 线程之间通信
4. 等待异步任务结果
5. 无锁原子操作

C++ 线程库不是直接操作 Linux pthread,而是在标准层面提供了一套跨平台接口。

std::thread:创建线程

最基础的线程类是 std::thread

#include <iostream>
#include <thread>

void work() {
    std::cout << "hello from thread\n";
}

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

    t.join();

    return 0;
}

std::thread t(work); 创建一个新线程执行 workt.join 表示主线程等待子线程执行结束。

join() 和 detach()

线程创建后,必须处理它的生命周期。

如上面的 t.join() 表示,当前线程等待 t 执行完

t.detach() 让线程后台运行,和当前 std::thread 对象分离,但是 detach() 很危险,因为线程后台运行时,如果访问了已经销毁的局部变量,就会出问题。

例如:

void bad() {
    int x = 10;

    std::thread t([&] {
        std::cout << x << std::endl;
    });

    t.detach();
}

bad() 返回后,x 已经销毁,后台线程再访问 x 就是悬空引用。

std::thread 析构前必须 join 或 detach

这是一个很重要的坑:

void f() {
    std::thread t([] {
        std::cout << "hello\n";
    });
}

这段代码会导致程序终止,因为 std::thread 对象析构时,如果线程仍然是 joinable 状态,标准库会调用:

std::terminate();

正确的写法是:

void f() {
    std::thread t([] {
        std::cout << "hello\n";
    });

    t.join();
}

判断线程是否还可以 join

if (t.joinable()) {
    t.join();
}

std::jthread:C++20 更安全的线程

这是 C++20 引入的特性,相比 std::thread 的优势是:

  1. 析构时自动 join
  2. 支持协作式停止 stop_token

示例:

#include <iostream>
#include <thread>

void work() {
    std::cout << "hello from jthread\n";
}

int main() {
    std::jthread t(work); //这里不需要手动 join(),std::jthread 析构时会自动等待线程结束。

    return 0;
}

为什么需要锁?

多个线程访问同一个共享变量时,可能出现数据竞争,这是很基本的概念:

#include <iostream>
#include <thread>

int counter = 0;

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

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

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

    std::cout << counter << std::endl;
}

预期的结果输出是 200000,但实际不一定,因为 ++count 不是原子操作,它大致包含:

1. 读取 counter
2. 加 1
3. 写回 counter

两个线程同时执行时可能互相覆盖结果,这就是数据竞争。

std::mutex:互斥锁

解决共享数据竞争最常见的方式是互斥锁:

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

int counter = 0;
std::mutex mtx;

void add() {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();
        ++counter;
        mtx.unlock();
    }
}

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

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

    std::cout << counter << std::endl;
}

但是手动 lock() / unlock() 有风险,如果在临界区中抛出异常:

mtx.lock();
// 出现异常
mtx.unlock();

锁可能永远无法释放,所以 C++ 更推荐 RAII 风格的锁管理器。

std::lock_guard

lock_guard 是最简单的 RAII 锁:

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

它的逻辑是构造时加锁,析构时自动释放,不需要手动 unlock,它适合的场景是:锁一个 mutex ,作用域结束自动释放。

std::unique_lock

unique_locklock_guard 更灵活,它支持:

/*
1. 延迟加锁
2. 手动 unlock
3. 转移所有权
4. 配合 condition_variable
*/
std::unique_lock<std::mutex> lock(mtx);

// 可以手动解锁
lock.unlock();

// 也可以重新加锁
lock.lock();

它最大的常见用途是配合条件变量:

std::condition_variable cv;
std::mutex mtx;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx);

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

    std::cout << "worker running\n";
}

std::scoped_lock

scoped_lock 是 C++17 引入的锁管理器,它可以同时锁多个 mutex,并避免死锁:

std::mutex m1;
std::mutex m2;

void f() {
    std::scoped_lock lock(m1, m2);

    // 同时持有 m1 和 m2
}

如果两个线程分别按照下面的情况加锁,就可能造成死锁:

// 线程 A
lock m1
lock m2

// 线程 B
lock m2
lock m1

std::scoped_lock lock(m1, m2); 内部会使用安全的多锁算法,避免这种加锁顺序导致的死锁。

简单比较:

工具特点
lock_guard最简单,锁一个 mutex
unique_lock最灵活,可手动解锁,可配合条件变量
scoped_lock可一次锁多个 mutex,避免多锁死锁

std::shared_mutex 和读写锁

普通 std::mutex 是独占锁,一次只有一个线程能进入临界区。但很多场景是读多写少,多个线程同时读是安全的,但必须写独占。

C++17 提供 std::shared_mutex 配合 std::unique_lock 就可以实现这个场景:

#include <shared_mutex>
#include <unordered_map>
#include <string>

std::unordered_map<std::string, int> cache;
std::shared_mutex mtx;

int get(const std::string& key) {
    std::shared_lock lock(mtx);  // 共享读锁

    auto it = cache.find(key);
    if (it != cache.end()) {
        return it->second;
    }

    return -1;
}

void set(const std::string& key, int value) {
    std::unique_lock lock(mtx);  // 独占写锁

    cache[key] = value;
}

/*
多个 get 可以并发执行
set 会独占锁
set 执行时不能有其他读或写
*/

适合读多写少的缓存、配置表、路由表等场景。

std::condition_variable:线程间等待和通知

条件变量用于:

一个线程等待某个条件成立,另一个线程修改条件后通知它。

经典生产者消费者模型:

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

std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;

void producer() {
    for (int i = 0; i < 10; ++i) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            q.push(i);
        }

        cv.notify_one();
    }
}

void consumer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);

        cv.wait(lock, [] {
            return !q.empty();
        });

        int value = q.front();
        q.pop();

        std::cout << value << std::endl;
    }
}

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

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

为什么 wait 要用 while / 谓词?

cv.wait(lock, [] {
    return !q.empty();
});

等价于:

while (q.empty()) {
    cv.wait(lock);
}

不能简单写:

if (q.empty()) {
    cv.wait(lock);
}

之前的 xv6 Multithread 实验 中提到过这一点,这里就简单说一下原因:

  1. 可能虚假唤醒
  2. 被唤醒后条件未必成立
  3. 多个线程被唤醒后,资源可能被别人先拿走

所以条件变量永远要搭配条件检查。

notify_one 和 notify_all

条件变量有两个通知接口:

cv.notify_one(); //唤醒一个等待线程
cv.notify_all(); //唤醒所有等待线程

std::atomic:原子操作

如果只是简单计数,可以不用 mutex,而用 std::mutex

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

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

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

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

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

    std::cout << counter.load() << std::endl;
}

std::atomic<int> 保证 ++count 是原子操作,不会被其他线程打断。

适合:

计数器 状态标记 引用计数 无锁队列中的基础原语

但注意 atomic 不等于万能。复杂临界区仍然需要 mutex

atomic 局限性

atomic 并不能保证目标变量是线程安全的,只能保证单个属性的单独读或单独者写是安全的,对于复合操作无法保证线程安全,例如下面的代码:

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

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

void foo() {
    for (int i = 0; i < 1000; i++) {
      count = count + 1;
    }
}

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

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

    std::cout << count << std::endl;

    return 0;
}

foo 函数中,每次循环先读取 count 的值,然后再 +1 赋值回去,虽然读取和赋值都是原子操作,但是两个原子操作形成的复合操作无法保证原子性。

例如:

t1 读取到 count = 0
t2 读取到 count = 0
t1 进行赋值操作 count = 1
t2 进行赋值操作 count = 1

结果获取到了错误值 count = 1(正确值为 2

atomic 和 mutex 怎么选?

简单规则:

场景选择
单个整数计数std::atomic<int>
bool 状态标记std::atomic<bool>
多个变量需要保持一致std::mutex
复杂容器修改std::mutex
生产者消费者阻塞等待condition_variable
读多写少shared_mutex

例如:

std::atomic<bool> stop{false};

适合做停止标志。

但如果要同时修改两个变量:

x++;
y++;

并且要求它们一致,就应该用 mutex 保护整个临界区。

std::future 和 std::async

有时候我们只是想异步执行一个任务,并拿到结果,可以用: std::asyncstd::future,例如:

#include <future>
#include <iostream>

int calculate() {
    return 42;
}

int main() {
    std::future<int> fut = std::async(std::launch::async, calculate);

    int result = fut.get();

    std::cout << result << std::endl;
}

std::async(std::launch::async, calculate); 表示异步执行 calculatefut.get() 会等待结果返回。

适合:

异步任务 一次性结果获取 不想手动管理 thread

std::promise 和 std::future

promise 用于一个线程主动设置结果,另一个线程通过 future 获取结果。

#include <future>
#include <iostream>
#include <thread>

void worker(std::promise<int> p) {
    p.set_value(100);
}

int main() {
    std::promise<int> p;
    std::future<int> f = p.get_future();

    std::thread t(worker, std::move(p));

    std::cout << f.get() << std::endl;

    t.join();
}

可以理解为:promise 生产结果,future 消费结果。

结尾

以下是一个完整的线程库应用示例:线程安全队列

#include <condition_variable>
#include <mutex>
#include <queue>
#include <optional>

template <typename T>
class ThreadSafeQueue {
public:
    void push(T value) {
        {
            std::lock_guard<std::mutex> lock(mtx_);
            q_.push(std::move(value));
        }

        cv_.notify_one();
    }

    T wait_and_pop() {
        std::unique_lock<std::mutex> lock(mtx_);

        cv_.wait(lock, [this] {
            return !q_.empty();
        });

        T value = std::move(q_.front());
        q_.pop();

        return value;
    }

    std::optional<T> try_pop() {
        std::lock_guard<std::mutex> lock(mtx_);

        if (q_.empty()) {
            return std::nullopt;
        }

        T value = std::move(q_.front());
        q_.pop();

        return value;
    }

private:
    std::queue<T> q_;
    std::mutex mtx_;
    std::condition_variable cv_;
};