C++ 并发编程是现代软件开发中的核心技术,主要用于利用多核处理器提升程序性能。C++11 及后续标准引入了完善的并发库(<thread>
、<mutex>
、<condition_variable>
等),使开发者能更安全地编写多线程程序。
1、std::thread
std::thread 是 C++11 引入的线程类,用于创建和管理线程。
1.1 基本使用
#include <thread>
#include <iostream>
using namespace std;// 线程函数:普通函数
void thread_func(int id) {cout << "Thread " << id << " 运行" << endl;
}int main() {// 创建线程(传入函数和参数)thread t1(thread_func, 1); thread t2([](int id) { // 线程函数:lambda表达式cout << "Thread " << id << " 运行" << endl;}, 2);// 等待线程结束(必须调用join()或detach(),否则程序崩溃)t1.join(); // 主线程阻塞,等待t1完成t2.join(); // 主线程阻塞,等待t2完成// 或分离线程(线程独立运行,主线程不等待)// t1.detach(); // t2.detach();return 0;
}
1.2 关键特性
join()
:主线程阻塞等待子线程完成,回收线程资源。detach()
:将线程与 std::thread 对象分离,线程后台运行,由系统自动回收资源(需确保线程访问的资源生命周期足够长)。- 线程对象必须被移动:
std::thread
不可拷贝,只能移动(thread t2 = move(t1)
;)。
2、互斥锁
互斥锁(Mutex)用于保护共享资源,确保同一时间只有一个线程访问资源,避免数据竞争。
多个线程同时读写共享数据会导致数据竞争,结果是未定义行为。
std::mutex
:基础互斥锁,提供lock()
(加锁)和unlock()
(解锁)方法(需手动配对,易出错)。std::lock_guard
:RAII 风格的锁管理,构造时自动加锁,析构时自动解锁(推荐,避免忘记解锁)。std::unique_lock
:更灵活的锁管理,支持手动 lock()/unlock()、超时等待等(适合条件变量)。
2.1 std::mutex
如果临界区中抛出异常,unlock()
可能不会被调用,导致死锁。
#include <thread>
#include <mutex>
#include <iostream>int shared_data = 0;
std::mutex data_mutex; // 用于保护 shared_datavoid increment() {for (int i = 0; i < 100000; ++i) {data_mutex.lock(); // 上锁++shared_data; // 临界区 (Critical Section)data_mutex.unlock(); // 解锁}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "Final value: " << shared_data << std::endl; // 总是 200000return 0;
}
2.2 lock_guard
这是推荐的做法,它在其作用域内自动管理锁的生命周期。
#include <mutex>
#include <vector>
#include <thread>vector<int> shared_data;
mutex mtx; // 保护shared_data的互斥锁void add_data(int val) {lock_guard<mutex> lock(mtx); // 构造时加锁,析构时解锁(离开作用域)shared_data.push_back(val); // 安全访问共享资源
}int main() {thread t1(add_data, 1);thread t2(add_data, 2);t1.join();t2.join();// shared_data 安全存储 [1,2] 或 [2,1]return 0;
}
2.3 std::unique_lock
比 std::lock_guard
更灵活(但开销稍大),可以延迟上锁、手动解锁、转移所有权。
void flexible_function() {std::unique_lock<std::mutex> ulock(data_mutex, std::defer_lock); // 延迟上锁// ... 做一些不需要锁的操作 ...ulock.lock(); // 现在需要锁了,手动上锁// ... 操作共享数据 ...ulock.unlock(); // 可以手动提前解锁// ... 更多操作 ...// ulock 析构时,如果还持有锁,会自动解锁
}
2.4 条件变量(std::condition_variable)
条件变量用于线程间通信,使线程能等待某个条件满足后再继续执行(如生产者 - 消费者模型)。
wait(lock, predicate)
:阻塞线程,释放锁并等待通知;被唤醒后重新获取锁,检查条件是否满足(不满足则继续等待)。notify_one()
:唤醒一个等待的线程。notify_all()
:唤醒所有等待的线程。
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>std::queue<int> data_queue;
std::mutex queue_mutex;
std::condition_variable data_cond;void data_preparation_thread() {int data = 0;while (true) {std::this_thread::sleep_for(std::chrono::seconds(1));{std::lock_guard<std::mutex> lk(queue_mutex);data_queue.push(++data);std::cout << "Prepared data: " << data << std::endl;} // lock_guard 超出作用域,锁释放data_cond.notify_one(); // 通知一个等待的消费者线程}
}void data_processing_thread() {while (true) {std::unique_lock<std::mutex> lk(queue_mutex);// 等待条件满足:Lambda 函数返回 true 时才继续,否则释放锁并等待通知data_cond.wait(lk, [] { return !data_queue.empty(); }); // 被 notify 后,重新获取锁,并检查条件int data = data_queue.front();data_queue.pop();lk.unlock(); // 处理数据不需要锁,提前解锁std::cout << "Processed data: " << data << std::endl;// 处理数据...}
}int main() {std::thread t1(data_preparation_thread);std::thread t2(data_processing_thread);t1.join();t2.join();
}
2.5 原子操作 (std::atomic)
std::atomic
提供无锁的原子操作,用于简单的计数器、标志位等场景,性能优于锁(无需上下文切换)。
#include <atomic>atomic<int> counter(0); // 原子计数器(线程安全)void increment() {for (int i = 0; i < 1000; ++i) {counter++; // 原子操作,无数据竞争}
}int main() {thread t1(increment);thread t2(increment);t1.join();t2.join();cout << "计数器结果: " << counter << endl; // 必然是2000(无竞争)return 0;
}
3、异步
3.1 std::async 和 std::future
std::async
用于启动异步任务,返回 std::future
对象,通过 future
获取任务结果(避免手动管理线程)。
#include <future>
#include <iostream>int calculate() {std::this_thread::sleep_for(std::chrono::seconds(2));return 42;
}int main() {// 启动一个异步任务// std::launch::async: 强制在新线程运行// std::launch::deferred: 延迟计算,直到调用 get() 时才在当前线程运行std::future<int> result_future = std::async(std::launch::async, calculate);std::cout << "Doing other work..." << std::endl;// 获取结果。如果计算未完成,会阻塞等待。int result = result_future.get(); std::cout << "The answer is: " << result << std::endl;// result_future.get() 只能调用一次!return 0;
}
3.2 std::promise 和 std::future
用于在线程之间传递结果,提供更精细的控制。
void do_work(std::promise<int> result_promise) {std::this_thread::sleep_for(std::chrono::seconds(1));result_promise.set_value(100); // 设置结果值// 如果发生异常: result_promise.set_exception(std::current_exception());
}int main() {std::promise<int> prom;std::future<int> fut = prom.get_future(); // 获取与 promise 关联的 futurestd::thread t(do_work, std::move(prom)); // 将 promise 移动到新线程int result = fut.get(); // 阻塞直到 promise 设置值std::cout << "Result: " << result << std::endl;t.join();return 0;
}
4、常见问题
-
什么是数据竞争 (Data Race)?如何避免?
当两个或多个线程在没有同步的情况下并发访问同一个内存位置,并且至少有一个是写操作时,就会发生数据竞争。其结果是不确定的,是未定义行为。
避免方法:- 使用互斥锁 (
std::mutex
) 保护共享数据。 - 将共享数据改为原子变量 (
std::atomic
)。 - 重新设计程序,避免共享(例如,每个线程处理自己的数据副本,最后合并)。
- 使用互斥锁 (
-
std::lock_guard
和std::unique_lock
有什么区别?std::lock_guard
:轻量级,作用域锁。构造时上锁,析构时解锁。不能手动控制锁的时机。std::unique_lock
:重量级,更灵活。可以延迟上锁、手动解锁、递归上锁、转移所有权。支持与条件变量一起使用。如果不需要std::unique_lock
的特殊功能,应优先使用std::lock_guard
。
-
什么是死锁 (Deadlock)?如何预防?
死锁是指两个或多个线程相互等待对方持有的资源,导致所有线程都无法继续执行的状态。
产生条件(四个必要条件,缺一不可):- 互斥:资源一次只能被一个线程持有。
- 持有并等待:线程持有一些资源,同时请求其他线程持有的资源。
- 不可剥夺:资源只能由持有它的线程主动释放。
- 循环等待:存在一个线程资源的循环等待链。
预防策略: - 按固定顺序上锁:所有线程以相同的全局顺序获取锁(例如,总是先锁 mutex A,再锁 mutex B)。
- 使用
std::lock()
函数:它可以一次性锁定多个互斥量,且不会产生死锁(内部使用死锁避免算法)。
std::mutex mutex1, mutex2; void safe_function() {std::lock(mutex1, mutex2); // 同时锁住两个,避免死锁std::lock_guard<std::mutex> lk1(mutex1, std::adopt_lock); // 接管已锁的mutex1std::lock_guard<std::mutex> lk2(mutex2, std::adopt_lock); // 接管已锁的mutex2// ... }
- 避免嵌套锁,或者使用超时机制 (
std::timed_mutex
)。
-
什么是虚假唤醒?
std::condition_variable::wait
为什么要用循环检查条件?
虚假唤醒是指等待中的线程在没有收到任何通知的情况下被操作系统唤醒。这是为了性能考虑,但会导致程序错误。
解决方法:始终在循环中检查等待条件。
data_cond.wait(lk, [] { return !data_queue.empty(); });
// 这等价于:
// while (!predicate()) { // 循环检查!
// data_cond.wait(lk);
// }
Lambda 函数(谓词)确保了即使发生虚假唤醒,线程也会再次检查条件是否真正满足,如果不满足会继续等待。
std::async
的启动策略std::launch::async
和std::launch::deferred
有什么区别?
std::launch::async
:异步执行。强制要求在新创建的线程上执行任务。std::launch::deferred
:延迟执行。任务不会立即执行,而是延迟到对返回的std::future
调用get()
或wait()
时,在调用者的线程中同步执行。- 如果不指定策略,标准允许实现自由选择,这可能导致不确定性。最佳实践是显式指定策略。
- 什么时候该用
std::atomic
,什么时候该用std::mutex
?
- 使用
std::atomic
:当你要保护的数据是简单的基本类型(如int
,bool
,指针
)并且操作是单一的(读、写、递增、交换等)时。性能开销远小于互斥锁。 - 使用
std::mutex
:- 当你要保护的数据是复杂的结构(如
std::vector
,std::map
)。 - 当你需要执行一组操作(临界区)来保持数据的不变性时。原子操作无法保证多个操作的整体原子性。
- 例如,检查一个
std::map
中是否存在某个键,如果不存在则插入,这个操作必须用互斥锁保护,原子操作无法完成。
- 当你要保护的数据是复杂的结构(如
-
如何实现一个线程安全的单例模式?
使用 Meyers' Singleton,它依靠静态局部变量的初始化在 C++11 及以后是线程安全的这一特性。class Singleton { public:static Singleton& getInstance() {static Singleton instance; // C++11保证此初始化是线程安全的return instance;}// 删除拷贝构造函数和赋值运算符以确保唯一性Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;void doSomething() { /* ... */ }private:Singleton() = default; // 私有构造函数~Singleton() = default; };// 使用 Singleton::getInstance().doSomething();
在 C++11 之前,需要使用双重检查锁定模式(Double-Checked Locking Pattern),但实现起来非常复杂且容易出错。Meyers' Singleton 是现代 C++ 中的最佳实践。
-
如何设计一个线程池?
线程池是管理多个线程的对象,避免频繁创建/销毁线程的开销,核心组件包括:- 工作线程:预先创建的线程,循环等待任务。
- 任务队列:存储待执行的任务(如
std::queue<std::function<void()>>
)。 - 互斥锁:保护任务队列的线程安全。
- 条件变量:通知工作线程有新任务。
简化实现思路:
class ThreadPool { private:vector<thread> workers; // 工作线程queue<function<void()>> tasks; // 任务队列mutex mtx;condition_variable cv;bool stop; // 停止标志// 工作线程函数:循环取任务执行void worker() {while (true) {function<void()> task;{unique_lock<mutex> lock(mtx);// 等待任务或停止信号cv.wait(lock, [this]{ return stop || !tasks.empty(); });if (stop && tasks.empty()) return;task = move(tasks.front());tasks.pop();}task(); // 执行任务}}public:// 构造函数:创建n个工作线程ThreadPool(size_t n) : stop(false) {for (size_t i = 0; i < n; ++i) {workers.emplace_back(&ThreadPool::worker, this);}}// 提交任务template<class F>void submit(F&& f) {{lock_guard<mutex> lock(mtx);tasks.emplace(std::forward<F>(f));}cv.notify_one(); // 通知工作线程}// 析构函数:停止所有线程~ThreadPool() {{lock_guard<mutex> lock(mtx);stop = true;}cv.notify_all(); // 唤醒所有线程for (auto& w : workers) {w.join(); // 等待线程结束}} };
-
条件变量(
condition_variable
)的wait()
为什么需要传入unique_lock
?wait()
需要在阻塞时释放锁(让其他线程有机会修改条件),被唤醒后重新获取锁(确保条件的安全性)。unique_lock
支持手动lock()
/unlock()
,而lock_guard
不支持手动解锁,因此wait()
必须使用unique_lock
。 -
std::async
的三种启动策略有什么区别?std::async
有三种启动策略(C++11 定义):std::launch::async
:立即创建新线程执行任务,任务与主线程并行。std::launch::deferred
:任务延迟执行,直到调用future::get()
或wait()
时,在当前线程同步执行。std::launch::async | std::launch::deferred
(默认):由系统决定策略(通常倾向于async
,但不保证)。