C++多线程编程—线程控制、同步与互斥详解
本文将深入探讨C++多线程编程中的核心概念:线程控制、同步与互斥。
1.线程控制:join 与 detach
当我们创建一个线程(std::thread)后,我们必须明确在这个线程对象销毁之前,如何管理它所代表的执行线程。这就是 join 和 detach 的用武之地。
join()
- 作用:阻塞当前线程(通常是主线程),等待被 join 的线程执行完毕后,再继续执行当前线程。
- 含义:“等待这个线程工作完成。”
- 注意:一个线程只能被 join 一次。
detach()
- 作用:将被 detach 的线程与 std::thread 对象分离,让该线程在后台独立运行。一旦分离,原 std::thread 对象不再代表任何线程,也无法再对它进行控制。
- 含义:“这个线程可以自己去流浪了,我不再管它了。”
- 注意:分离后的线程通常称为守护线程。确保线程中访问的数据在其生命周期内有效是程序员的责任。主线程退出时,所有分离的线程也会被强制终止。
#include <iostream> #include <thread> #include <chrono> void worker() { std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "子线程工作完成...\n"; } int main() { std::thread t(worker); // 主线程在此阻塞,等待 t 执行完毕 // t.join(); // 或者,将 t 分离,让其后台运行 t.detach(); // 注意:detach 后,主线程不会等待 worker std::cout << "主线程继续执行...\n"; std::this_thread::sleep_for(std::chrono::seconds(2)); // 防止主线程过早结束杀死detach的线程 // 错误!线程分离或join后,不能再join // if (t.joinable()) t.join(); return 0; }
输出(使用 join):
子线程工作完成...
主线程继续执行...
输出(使用 detach):
主线程继续执行...
子线程工作完成...
2. 线程的互斥:std::mutex
当多个线程需要访问共享数据时,如果不加控制,会导致数据竞争,引发未定义行为。std::mutex(互斥锁)是最基本的同步原语,用于保护共享数据,确保一次只有一个线程可以访问。
lock() 与 unlock()
- lock():尝试获取锁。如果锁已被其他线程持有,则当前线程被阻塞,直到获取到锁。
- unlock():释放锁,允许其他被阻塞的线程获取它。
注意:必须成对使用 lock() 和 unlock(),否则会导致死锁。为了避免忘记 unlock(),推荐使用RAII机制的包装类,如 std::lock_guard。
#include <iostream> #include <thread> #include <mutex> std::mutex g_mutex; int shared_data = 0; void increment() { for (int i = 0; i < 100000; ++i) { g_mutex.lock(); // 获取锁 ++shared_data; // 临界区操作 g_mutex.unlock(); // 释放锁 } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "最终结果: " << shared_data << std::endl; // 总是 200000 return 0; }
3. RAII锁管理:std::lock_guard 和 std::unique_lock
手动管理 lock 和 unlock 非常容易出错。C++提供了RAII风格的锁管理类,在构造时加锁,析构时自动解锁,极大地提高了代码的安全性和简洁性。
std::lock_guard
- 特点:简单、轻量级、不可复制。它在构造时锁定互斥量,在析构时自动解锁。它没有提供手动加锁或解锁的接口。
- 适用场景:简单的临界区,整个作用域都需要加锁。
void safe_increment_lock_guard() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(g_mutex); // 构造时加锁,析构时解锁 ++shared_data; } }
std::unique_lock
- 特点:比 std::lock_guard 更灵活但稍重。它支持延迟锁定、手动锁定、解锁、条件变量等。
- 适用场景:需要更灵活锁管理的场景,例如需要提前解锁或与条件变量配合使用。
void safe_increment_unique_lock() { for (int i = 0; i < 100000; ++i) { std::unique_lock<std::mutex> lock(g_mutex); // 同样,构造时加锁 ++shared_data; lock.unlock(); // 可以提前解锁,减少锁的持有时间 // ... 执行一些不共享的操作 // 不需要再手动lock,因为析构函数会检查状态,如果已解锁则什么都不做 } }
4. 线程的同步:std::condition_variable
互斥锁解决了数据竞争问题,但线程间有时需要协同工作。例如,一个线程需要等待另一个线程完成任务或满足某个条件后再继续执行。这就是条件变量 std::condition_variable 的作用。它通常与 std::mutex(尤其是 std::unique_lock)配合使用。
核心操作:
wait(lock, predicate):
- 原子地解锁 lock 并阻塞当前线程。
- 当被 notify_one() 或 notify_all() 唤醒时,线程会重新获取锁并检查 predicate(可调用对象,返回bool)。
- 如果 predicate 返回 true,则 wait 返回,线程继续执行。
- 如果 predicate 返回 false,则线程再次解锁并阻塞(predicate 参数可省略,但建议使用以避免虚假唤醒)
- notify_one():随机唤醒一个正在等待此条件变量的线程。
- notify_all():唤醒所有正在等待此条件变量的线程。
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> std::mutex mtx; std::condition_variable cv; std::queue<int> data_queue; const int MAX_ITEMS = 10; void producer() { for (int i = 0; i < MAX_ITEMS; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::unique_lock<std::mutex> lock(mtx); // 如果队列已满,生产者等待消费者消费,注意lock为变量名 cv.wait(lock, [] { return data_queue.size() < MAX_ITEMS; }); data_queue.push(i); std::cout << "生产: " << i << std::endl; lock.unlock(); // 提前解锁 cv.notify_one(); // 通知一个消费者 } } void consumer() { while (true) { std::unique_lock<std::mutex> lock(mtx); // 如果队列为空,消费者等待生产者生产 cv.wait(lock, [] { return !data_queue.empty(); }); int data = data_queue.front(); data_queue.pop(); std::cout << "消费: " << data << std::endl; if (data == MAX_ITEMS - 1) break; // 简单退出条件 lock.unlock(); cv.notify_one(); // 通知一个生产者 } } int main() { std::thread p(producer); std::thread c(consumer); p.join(); c.join(); return 0; }

浙公网安备 33010602011771号