1. 核心头文件
要使用 C++ 多线程,首先需要包含以下头文件:
cpp
#include <thread> // 线程管理(创建、休眠、获取ID等) #include <mutex> // 互斥锁(std::mutex, std::lock_guard, std::unique_lock) #include <atomic> // 原子操作(无锁编程) #include <future> // 异步操作(std::async, std::promise, std::future) #include <condition_variable> // 条件变量(线程间同步)
2. 创建线程
创建线程最基本的方式是使用 std::thread
类,其构造函数接受一个可调用对象(函数、Lambda表达式、函数对象等)以及该对象所需的参数。
示例1:使用函数
cpp
#include <iostream> #include <thread> void helloFunction() { std::cout << "Hello from function! Thread ID: " << std::this_thread::get_id() << std::endl; } int main() { // 创建线程对象 t1,并立即执行 helloFunction std::thread t1(helloFunction); // 使用 Lambda 表达式创建线程 t2 std::thread t2([](){ std::cout << "Hello from lambda! Thread ID: " << std::this_thread::get_id() << std::endl; }); // 等待两个线程执行完毕 t1.join(); // main 线程阻塞,直到 t1 完成 t2.join(); // main 线程阻塞,直到 t2 完成 std::cout << "Main thread done." << std::endl; return 0; }
重要提示:
join()
: 等待子线程结束,然后继续执行主线程。必须在你创建的std::thread
对象被销毁前调用join()
或detach()
,否则程序会调用std::terminate
终止。detach()
: 将子线程与主线程分离,使其成为守护线程(daemon thread),在后台独立运行。一旦分离,你将无法再与之交互。通常用于长时间运行的任务。
3. 数据共享与竞态条件
当多个线程读写同一块共享数据时,如果没有任何同步措施,程序的执行结果将变得不确定,这就是竞态条件。
示例:一个有问题的计数器
cpp
#include <thread> #include <iostream> int counter = 0; void incrementCounter(int numIterations) { for (int i = 0; i < numIterations; ++i) { ++counter; // 这不是原子操作! } } int main() { const int numIterations = 1000000; std::thread t1(incrementCounter, numIterations); std::thread t2(incrementCounter, numIterations); t1.join(); t2.join(); // 结果几乎肯定不是 2000000 std::cout << "Final counter value: " << counter << std::endl; return 0; }
++counter
看起来是一条语句,但底层对应读取-修改-写入三条指令,线程可能会在这三条指令之间被中断,导致数据覆盖。
4. 使用互斥锁保护共享数据
互斥锁(Mutex) 是最常用的同步原语。它保证同一时间只有一个线程能进入被保护的代码段(临界区)。
a. std::mutex
和 std::lock_guard
std::lock_guard
是一个 RAII 类,在构造时自动加锁,在析构时自动解锁,避免了手动解锁的麻烦,异常安全。
cpp
#include <thread> #include <iostream> #include <mutex> int counter = 0; std::mutex counter_mutex; // 保护 counter 的互斥锁 void safeIncrement(int numIterations) { for (int i = 0; i < numIterations; ++i) { std::lock_guard<std::mutex> lock(counter_mutex); // 构造时加锁 ++counter; // 临界区 // lock 析构时自动解锁 } } int main() { const int numIterations = 1000000; std::thread t1(safeIncrement, numIterations); std::thread t2(safeIncrement, numIterations); t1.join(); t2.join(); // 结果总是 2000000 std::cout << "Final counter value: " << counter << std::endl; return 0; }
b. std::unique_lock
std::unique_lock
比 std::lock_guard
更灵活(但开销稍大),可以延迟加锁、手动解锁/加锁,并且是条件变量所必需的。
cpp
std::mutex mtx; void complexFunction() { std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁 // ... do some work that doesn't need the lock ... lock.lock(); // 现在手动加锁 // ... critical section ... lock.unlock(); // 可以手动解锁 // ... more non-critical work ... // 如果锁还在,析构时会自动解锁 }
5. 原子操作
对于简单的计数器,使用 std::atomic
类型是更轻量级且高效的选择。它通过硬件指令保证操作的原子性,无需锁。
cpp
#include <atomic> #include <thread> #include <iostream> std::atomic<int> atomic_counter(0); // 原子整数 void atomicIncrement(int numIterations) { for (int i = 0; i < numIterations; ++i) { ++atomic_counter; // 这是一个原子操作 // atomic_counter.fetch_add(1, std::memory_order_relaxed); // 也可以这样写 } } int main() { const int numIterations = 1000000; std::thread t1(atomicIncrement, numIterations); std::thread t2(atomicIncrement, numIterations); t1.join(); t2.join(); // 结果总是 2000000,且性能比用互斥锁高 std::cout << "Final atomic counter value: " << atomic_counter << std::endl; return 0; }
适用场景:适用于单个变量(整数、指针、甚至自定义结构,如果满足特定条件)的简单读写、递增、比较交换(CAS)等操作。
6. 条件变量
条件变量允许线程在某个条件不满足时主动阻塞(睡眠),直到另一个线程通知条件可能已改变。它必须和互斥锁一起使用。
经典模式:生产者-消费者
cpp
#include <thread> #include <mutex> #include <condition_variable> #include <queue> #include <iostream> std::queue<int> data_queue; std::mutex mtx; std::condition_variable cv; void data_producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟生产耗时 { std::lock_guard<std::mutex> lock(mtx); data_queue.push(i); std::cout << "Produced: " << i << std::endl; } cv.notify_one(); // 通知一个等待的消费者 } } void data_consumer() { while (true) { std::unique_lock<std::mutex> lock(mtx); // 等待条件:队列不为空。lambda 表达式是谓词,防止虚假唤醒 cv.wait(lock, []{ return !data_queue.empty(); }); int data = data_queue.front(); data_queue.pop(); lock.unlock(); // 尽早释放锁 std::cout << "Consumed: " << data << std::endl; if (data == 9) break; // 简单终止条件 } } int main() { std::thread producer(data_producer); std::thread consumer(data_consumer); producer.join(); consumer.join(); return 0; }
cv.wait(lock, predicate)
:会原子地解锁lock
并阻塞当前线程。当被notify
唤醒时,它会重新获取锁,并检查predicate
。如果predicate
返回false
,它会继续等待。防止虚假唤醒:使用带谓词的
wait
是标准做法,因为条件变量可能因为系统原因被意外唤醒。
7. 异步操作与 Future/Promise
<future>
头文件提供了更高层次的抽象,用于获取异步任务(在其他线程上运行的任务)的结果。
a. std::async
和 std::future
最简单的启动异步任务的方式。
cpp
#include <future> #include <iostream> int expensiveCalculation(int x) { std::this_thread::sleep_for(std::chrono::seconds(2)); return x * x; } int main() { // 启动一个异步任务 // std::launch::async 保证在新线程中执行 // std::launch::deferred 表示延迟计算(直到调用 get() 时才在当前线程执行) std::future<int> result_future = std::async(std::launch::async, expensiveCalculation, 10); std::cout << "Doing some other work..." << std::endl; // .get() 会阻塞,直到异步任务完成并返回结果 int result = result_future.get(); std::cout << "Result: " << result << std::endl; // Output: Result: 100 return 0; }
b. std::promise
和 std::future
std::promise
允许你在一个线程中设置一个值(或异常),并通过与之关联的 std::future
在另一个线程中获取它。这是一种更手动的线程间传递结果的机制。
cpp
void doWork(std::promise<int> prom) { std::this_thread::sleep_for(std::chrono::seconds(2)); prom.set_value(42); // 设置结果 } int main() { std::promise<int> myPromise; std::future<int> myFuture = myPromise.get_future(); std::thread worker(doWork, std::move(myPromise)); // promise 不可复制,只能移动 std::cout << "Waiting for the result..." << std::endl; int result = myFuture.get(); // 阻塞并获取结果 std::cout << "The answer is: " << result << std::endl; worker.join(); return 0; }
最佳实践与注意事项
优先使用 RAII:始终使用
std::lock_guard
或std::unique_lock
来管理锁,而不是手动调用lock()
和unlock()
,以确保异常安全。缩小临界区:锁的粒度要细。只在绝对必要的时候持有锁,锁住后尽快释放。
避免死锁:以固定的顺序获取多个锁(例如,总是先锁 mutex A,再锁 mutex B),或者使用
std::lock(m1, m2, ...)
来一次性锁住多个互斥量而避免死锁。考虑无锁编程:对于简单的数据,优先考虑
std::atomic
。线程不宜过多:线程的创建和上下文切换有开销。线程数量通常与 CPU 核心数相匹配是较好的起点。对于 I/O 密集型任务,可以多一些。
使用高级抽象:如果可能,优先使用
std::async
和std::future
,而不是手动管理std::thread
和同步原语,这可以减少错误。注意线程安全:标准库的容器和函数通常不是线程安全的(除了像
std::atomic
这样的特例)。多个线程读写同一个容器必须手动加锁。
C++ 多线程编程是一个庞大而复杂的主题,这里只是冰山一角。但掌握了这些核心概念(thread
, mutex
, atomic
, condition_variable
, future
),已经可以应对绝大多数多线程开发场景了。实践中,务必谨慎小心,多使用线程检查工具(如 ThreadSanitizer)来发现潜在的竞态条件和死锁。