C++ 多线程
使用标准库进行 C++ 多线程编程
Section titled “使用标准库进行 C++ 多线程编程”多线程(Multithreading)允许程序并发地执行多个任务,从而提高性能和响应速度,尤其是在多核处理器上。现代 C++ 直接在标准库(自 C++11 起)中提供了强大的多线程支持,主要通过 <thread>、<mutex>、<condition_variable> 和 <future> 等头文件实现。
虽然存在操作系统特定的 API,例如 POSIX 线程(pthreads)或 Windows 线程,并且有时会用于平台特定功能,但 C++ 标准库提供了一种可移植且类型安全的方式来管理线程和同步。
关键概念:
- 线程(Thread): 进程内独立执行的路径。
- 并发(Concurrency): 多个线程随着时间的推移取得进展(可能涉及在单核上的切换)。
- 并行(Parallelism): 多个线程在不同的核心上同时执行。
- 同步(Synchronization): 用于协调线程间对共享资源的访问并防止数据竞争(data races)的机制(如互斥量 mutexes、条件变量 condition variables)。
使用 std::thread 创建线程
Section titled “使用 std::thread 创建线程”<thread> 头文件提供了 std::thread 类来创建和管理线程。通过构造一个 std::thread 对象来创建一个线程,需要传递一个可调用实体(callable entity)(函数、lambda 表达式、函数对象 functor)及其参数。
#include <iostream>#include <thread> // 需要包含 std::thread#include <vector>#include <string>
// 将由线程执行的函数void print_message(int id, const std::string& message) { // std::this_thread::get_id() 返回当前线程的唯一 ID std::cout << "Thread [" << std::this_thread::get_id() << "] ID: " << id << " says: " << message << std::endl;}
int main() { std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;
// 创建线程 t1 执行 print_message(1, "Hello from thread 1") std::thread t1(print_message, 1, "Hello from thread 1");
// 使用 lambda 表达式创建线程 t2 int thread_id_2 = 2; std::thread t2([thread_id_2]() { // 按值捕获 thread_id_2 std::cout << "Thread [" << std::this_thread::get_id() << "] ID: " << thread_id_2 << " says: Hi from lambda!" << std::endl; });
// 创建多个线程 std::vector<std::thread> threads; for (int i = 3; i <= 5; ++i) { // 使用 emplace_back 在 vector 中直接构造线程 threads.emplace_back(print_message, i, "Greetings from thread " + std::to_string(i)); }
std::cout << "主线程继续执行..." << std::endl;
// 等待线程完成 (Join) std::cout << "等待线程完成..." << std::endl; t1.join(); t2.join(); for (auto& t : threads) { t.join(); }
std::cout << "所有线程已完成。" << std::endl; return 0;}使用 C++11(或更高版本)兼容的编译器编译,并在需要时链接线程库(通常是 GCC/Clang 的 -pthread 或 -lpthread 标志):
g++ your_file.cpp -o thread_example -std=c++11 -pthread由于并发执行,来自不同线程的消息的输出顺序是不确定的。
加入(Joining)和分离(Detaching)线程
Section titled “加入(Joining)和分离(Detaching)线程”一个 std::thread 对象在其超出作用域或被销毁之前,必须被 join(加入)或 detach(分离)。否则会导致调用 std::terminate。
join(): 使调用线程(例如主线程)阻塞并等待关联线程(t1、t2等)执行完成。这是管理线程生命周期最常见的方式。detach(): 将执行线程与std::thread对象分离。线程在后台独立继续执行。原始的std::thread对象不再代表正在运行的线程,也无法被 join。分离的线程必须谨慎管理自己的生命周期;如果main函数退出,分离的线程通常会突然终止。请谨慎使用detach()。
// 分离示例(请谨慎使用!)// std::thread t3(background_task);// t3.detach(); // t3 对象不再管理该线程// // 主线程继续执行,无需等待 background_task在构造 std::thread 时,通过复制或移动参数的方式将其传递给线程函数。请注意参数类型:
- 按值(By Value): 参数被复制。线程函数内部的修改不影响原始变量。
- 按引用(By Reference): 要按引用传递参数,请使用
std::ref()或std::cref()(用于 const 引用)包裹它们。 - 按指针(By Pointer): 指针可以直接传递,但要确保指向的对象在线程的生命周期内保持有效(注意悬空指针)。
- 移动语义(Move Semantics): 支持移动语义的对象可以被移动到线程中,从而转移所有权。
#include <iostream>#include <thread>#include <string>#include <functional> // 用于 std::ref, std::cref
void modify_value(int& value) { // 按引用接收 int value *= 2; std::cout << "Thread modified value to: " << value << std::endl;}
void read_value(const int& value) { // 按 const 引用接收 int std::cout << "Thread read value: " << value << std::endl;}
int main() { int shared_data = 10;
// 使用 std::ref 按引用传递 std::thread t_modify(modify_value, std::ref(shared_data)); t_modify.join(); // 等待修改完成 std::cout << "Main thread sees shared_data as: " << shared_data << std::endl; // 应该是 20
// 使用 std::cref 或 std::ref 按 const 引用传递 std::thread t_read(read_value, std::cref(shared_data)); t_read.join();
return 0;}同步:互斥量(Mutex)
Section titled “同步:互斥量(Mutex)”当多个线程访问共享数据,并且至少一个线程修改它时,您需要同步来防止数据竞争(data races)(未定义行为)。互斥量(Mutex,Mutual Exclusion)是最基本的同步原语。
<mutex> 头文件提供了 std::mutex。线程在访问共享数据之前锁定(lock)互斥量,并在之后解锁(unlock)。任何时候只有一个线程可以持有锁。
强烈推荐使用 RAII(Resource Acquisition Is Initialization,资源获取即初始化)封装器,例如 std::lock_guard 或 std::scoped_lock (C++17)。它们在构造时自动锁定互斥量,并在析构时(当它们超出作用域时)自动解锁,确保互斥量总是被释放,即使发生异常。
#include <iostream>#include <thread>#include <vector>#include <mutex> // 需要包含 std::mutex 和 std::lock_guard#include <chrono> // 用于 std::chrono::milliseconds#include <thread> // 用于 std::this_thread::sleep_for
std::mutex cout_mutex; // 用于保护 std::cout 访问的互斥量int shared_counter = 0;std::mutex counter_mutex; // 用于保护 shared_counter 访问的互斥量
void increment_counter(int id) { for (int i = 0; i < 5; ++i) { // 在访问 shared_counter 之前锁定 counter_mutex // std::lock_guard 在 'lock' 超出作用域时自动解锁 std::lock_guard<std::mutex> lock(counter_mutex); shared_counter++;
// 在打印之前锁定 cout_mutex { std::lock_guard<std::mutex> cout_lock(cout_mutex); std::cout << "Thread " << id << " incremented counter to " << shared_counter << std::endl; } // cout_lock 在此处释放
// 模拟一些工作 std::this_thread::sleep_for(std::chrono::milliseconds(10));
} // lock (针对 counter_mutex) 在此处释放}
int main() { std::vector<std::thread> threads; for (int i = 0; i < 3; ++i) { threads.emplace_back(increment_counter, i + 1); }
for (auto& t : threads) { t.join(); }
std::cout << "Final counter value: " << shared_counter << std::endl; // 应该是 15 return 0;}其他同步原语
Section titled “其他同步原语”C++ 标准库提供了其他工具,用于更复杂的同步场景:
std::condition_variable(条件变量): 允许线程高效地等待特定条件变为真。std::atomic(原子类型): 提供基本类型的原子操作(不可分割、不可中断),对于简单的计数器或标志通常比互斥量更高效。std::future和std::promise/std::async: 使得从异步操作(可能在其他线程中运行的任务)中检索结果成为可能。
多线程引入了复杂性(调试、潜在的死锁 deadlocks、活锁 livelocks)。从简单的开始,使用 RAII 管理资源(锁),并仔细设计线程间的交互。