Skip to content

C++ 多线程

多线程(Multithreading)允许程序并发地执行多个任务,从而提高性能和响应速度,尤其是在多核处理器上。现代 C++ 直接在标准库(自 C++11 起)中提供了强大的多线程支持,主要通过 <thread>、<mutex>、<condition_variable> 和 <future> 等头文件实现。

虽然存在操作系统特定的 API,例如 POSIX 线程(pthreads)或 Windows 线程,并且有时会用于平台特定功能,但 C++ 标准库提供了一种可移植且类型安全的方式来管理线程和同步。

关键概念:

  • 线程(Thread): 进程内独立执行的路径。
  • 并发(Concurrency): 多个线程随着时间的推移取得进展(可能涉及在单核上的切换)。
  • 并行(Parallelism): 多个线程在不同的核心上同时执行。
  • 同步(Synchronization): 用于协调线程间对共享资源的访问并防止数据竞争(data races)的机制(如互斥量 mutexes、条件变量 condition variables)。

<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;
}

当多个线程访问共享数据,并且至少一个线程修改它时,您需要同步来防止数据竞争(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;
}

C++ 标准库提供了其他工具,用于更复杂的同步场景:

  • std::condition_variable (条件变量): 允许线程高效地等待特定条件变为真。
  • std::atomic (原子类型): 提供基本类型的原子操作(不可分割、不可中断),对于简单的计数器或标志通常比互斥量更高效。
  • std::future 和 std::promise / std::async: 使得从异步操作(可能在其他线程中运行的任务)中检索结果成为可能。

多线程引入了复杂性(调试、潜在的死锁 deadlocks、活锁 livelocks)。从简单的开始,使用 RAII 管理资源(锁),并仔细设计线程间的交互。