多线程编程

多线程编程,并发编程的一种,即在同一个进程中执行多个线程,每个线程都有独立的栈、指令计数器,但共享同一个内存空间(堆、全局变量等),可以让程序在多核 CPU 上并行执行,从而更快更高效喵!。

但是由于缺少系统的保护机制,多线程编程容易出现数据竞争和死锁等问题。

C++11 中的多线程

编译器版本:Clang 21.1.1

C++ 标准:C++11

多线程在 C++11 被引入,其工具集在 C++11 已经比较完善,主要分为五个板块:

  1. 线程管理:thread
  2. 互斥锁:mutex
  3. 线程同步:condition_variable
  4. 原子操作:atomic
  5. 异步操作:futureasync

thread 线程类

std::thread 单个执行线程的类,用于创建和管理线程。

构造函数

1
2
std::thread t1; //  默认构造函数,默认不执行
std::thread t2(func, arg1, arg2); // 构造函数,传入函数和参数,执行线程

成员函数

  • join():等待线程执行结束,阻塞当前线程,直到线程执行结束。
  • detach():将线程与 thread 对象分离,允许线程独立执行(守护线程)。
  • joinable:检查线程是否可被 join(),即在运行且未被分离。
  • get_id() :获取线程的唯一标识符。
  • hardware_concurrency():静态函数,返回系统硬件支持的并发线程数。

mutex 互斥锁类

std::mutex 保护共享数据,防止多个线程同时访问导致数据竞争。

std::mutex

最基本的互斥锁,不可递归锁定。

成员函数

  • lock():获取互斥锁,如果互斥锁已经被其他线程锁定,则阻塞当前线程,直到互斥锁被释放。
  • try_lock():尝试获取互斥锁,如果互斥锁已经被其他线程锁定,则立即返回 false,否则获取锁并返回 true
  • unlock():释放互斥锁。

std::recursive_mutex

允许同一线程多次锁定同一个互斥锁。

1
2
3
4
5
6
7
8
std::recursive_mutex rec_mtx;

void recursive_function(int depth) {
std::lock_guard<std::recursive_mutex> lock(rec_mtx);
if (depth > 0) {
recursive_function(depth - 1); // 可以递归调用
}
}

std::timed_mutex

带超时功能的互斥锁。

成员函数

  • try_lock_for(const chrono::duration<Rep, Period>& timeout_duration):尝试获取互斥锁,如果互斥锁已经被其他线程锁定,则在指定时间内阻塞当前线程,直到互斥锁被释放或超时,成功获取锁返回 true,超时返回 false
  • try_lock_until(const chrono::time_point<chrono::system_clock, chrono::duration<Rep, Period>>& timeout_time):尝试获取互斥锁,如果互斥锁已经被其他线程锁定,则在时间点之前阻塞当前线程,直到互斥锁被释放或超时,成功获取锁返回 true,超时返回 false

std::lock_guard

RAII 风格的锁管理器,构造时锁定,析构时自动解锁。

1
2
3
4
5
std::mutex mtx;
{
std::lock_guard<std::mutex> lock(mtx);
// 代码块
}// 自动解锁

std::unique_lock

更灵活的锁管理器,可以选择手动锁定和解锁,也可以选择超时时间。

成员函数

  • lock():手动锁定互斥锁。
  • try_lock():尝试手动锁定互斥锁,如果互斥锁已经被其他线程锁定,则立即返回 false,否则获取锁并返回 true
  • unlock():手动解锁互斥锁。
  • release():释放所有权,不解锁。
  • defer_lock:创建但不锁定互斥锁。
  • adopt_lock:接管已加锁的互斥量,避免重复加锁。
1
2
3
4
5
6
7
8
9
10
11
std::mutex mtx1, mtx2;
{
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock); // 创建,但不锁定
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock); // 创建,但不锁定
lock1.lock(); // 手动锁定 mtx1
lock2.lock(); // 手动锁定 mtx2
// 也可使用 std::lock(lock1, lock2) 一次性锁定

// 代码块
lock1.unlock(); // 手动解锁 mtx1
} // 自动解锁 mtx2

condition_variable 条件变量类

  • 允许一个或多个线程等待某个条件成立。
  • 其他线程可以通过 notify_one()notify_all() 来唤醒等待的线程。
  • 等待线程会自动释放锁,进入阻塞状态,直到被唤醒并重新获得锁。

成员函数

  • wait(lock):使当前线程阻塞,直到被通知。
  • wait(lock,pred):使当前线程阻塞,直到被通知且 pred() 返回 true
  • notify_one():通知一个等待线程。
  • notify_all():通知所有等待线程。
  • wait_for():等待指定时间,直到被通知且条件满足或者超时,条件满足返回 true,超时返回 false
  • wait_until():等待到指定时间点,直到被通知且条件满足或者超时,条件满足返回 true,超时返回 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;

void producer() {
for (int i = 0; i < 5; ++i) {
std::unique_lock<std::mutex> lock(mtx);
q.push(i);
std::cout << "Produced: " << i << "\n";
cv.notify_one(); // 通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}

void consumer() {
for (int i = 0; i < 5; ++i) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !q.empty(); }); // 等待队列非空
int val = q.front(); q.pop();
std::cout << "Consumed: " << val << "\n";
}
}

int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}

atomic 原子操作类

用于在多线程环境中执行无锁的原子操作,从而避免数据竞争并提升性能。

构造函数

1
2
3
std::atomic<int> counter(0);
std::atomic<bool> flag(true);
std::atomic<void*> ptr(nullptr);

成员函数

  • load():获取原子变量的值。
  • store():设置原子变量的值。
  • fetch_add():将原子变量的值加上指定值,并返回原值。
  • fetch_sub():将原子变量的值减去指定值,并返回原值。
  • exchange():交换值并返回原值。
  • compare_exchange_strong(expected, desired):如果当前值等于 expected,则将原子变量的值设置为 desired,并返回 true;否则,返回 false
  • compare_exchange_weak(expected, desired):基本同 compare_exchange_strong
`strong` 和 `weak` 的区别

某些平台的硬件指令(如 ARM)在实现 CAS 时可能会偶尔失败,即使值匹配。

compare_exchange_strong 不允许虚假失败;但 compare_exchange_weak 允许,即值相同也可能失败,但同时性能更高。

所以 compare_exchange_strong 比较适合用于确保原子操作的成功,而 compare_exchange_weak 适合用于提升性能(在循环中使用)。

内存序

在多线程程序中,编译器和 CPU 为了优化性能,可能会对指令进行重排,这意味着你写在前面的代码,可能在执行时被放到后面,或者被其他线程看到的顺序不同。

内存序(memoryorder)就是用来控制这种重排行为的机制,确保线程之间的操作顺序符合预期。

内存序同步相关重拍相关应用场景
memory_order_relaxed不同步允许重排高性能计数器、无依赖场景
memory_order_acquire同步之前写入禁止后面的重排到前面读取标志位后读取数据
memory_order_release同步之后读取禁止前面的重排到后面写入数据后设置标志位
memory_order_acq_rel同步前后双向禁止重排读写结合的同步点
memory_order_seq_cst同步所有线程全局顺序一致默认,最安全但性能差

async 异步操作类

标准库引入了一整套用于 异步操作 的类和机制,使得多线程编程更加现代化和易用。

std::async

异步执行函数,返回 std::future

构造函数

  • std::launch::async:立即在新线程中执行。
  • std::launch::deferred:延迟执行,直到 std::future::get() 被调用。
1
2
3
std::future<T> f = std::async(std::launch::deferred, func, arg1, arg2);

f.get(); // 阻塞,直到结果可用

std::future

提供对异步操作结果的访问,表示一个尚未完成的异步操作的结果。

成员函数

  • get():等待异步操作完成,并返回结果。
  • wait():等待异步操作完成。
  • wait_for():等待指定时间,直到异步操作完成或超时。
  • valid():检查异步操作是否有效。
  • get_future():从 promisepackaged_task 获取 std::future 对象。

std::promise

存储值或异常,供 std::future 读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void worker(std::promise<int> prom) {
try {
int result = perform_calculation();
prom.set_value(result); // 设置结果
} catch (...) {
prom.set_exception(std::current_exception()); // 设置异常
}
}

std::promise<int> prom;
std::future<int> fut = prom.get_future(); // 获取 future

std::thread t(producer, std::ref(prom));

fut.get(); // 阻塞直到结果可用
t.join();

std::packaged_task

将可调用对象包装为异步任务。

1
2
3
4
5
6
7
std::packaged_task<int(int,int)> task(compute);
std::future<int> fut = task.get_future();

// 在单独线程中执行
std::thread t(std::move(task), 10, 20);
int result = fut.get();
t.join();

实用函数

  • sleep_for():使当前线程睡眠指定时间。
  • sleep_until():使当前线程睡眠到指定时间点。
  • yield():让出当前线程的执行权。