现代C++多线程编程的核心工具就是std::thread。这个类在C++11标准中引入,彻底改变了C++开发者处理并发任务的方式。与传统的pthread或Windows API相比,std::thread提供了更高层次的抽象,让多线程编程变得更加直观和安全。
创建一个线程最基本的语法是构造一个std::thread对象,传入可调用对象(函数、lambda表达式等)及其参数。下面这个简单示例展示了最基本的线程创建和等待:
cpp复制#include <iostream>
#include <thread>
void print_number(int num) {
std::cout << "Number: " << num << std::endl;
}
int main() {
std::thread worker(print_number, 42);
worker.join();
std::cout << "Main thread continues..." << std::endl;
return 0;
}
关键点:join()会阻塞当前线程,直到worker线程执行完毕。如果不调用join()或detach(),程序终止时会调用std::terminate()。
默认情况下,std::thread的参数是按值传递的。这意味着线程函数接收的是参数的副本。如果需要传递引用,必须使用std::ref或std::cref包装:
cpp复制void modify_string(std::string& str) {
str += " modified";
}
int main() {
std::string data = "original";
std::thread t1(modify_string, std::ref(data));
t1.join();
std::cout << data << std::endl; // 输出:"original modified"
}
常见陷阱:直接传递引用而不使用std::ref会导致编译错误,因为线程构造函数会尝试复制引用本身而非被引用的对象。
对于不可复制但可移动的对象(如unique_ptr),可以使用std::move:
cpp复制void process_data(std::unique_ptr<int> ptr) {
std::cout << *ptr << std::endl;
}
int main() {
auto ptr = std::make_unique<int>(42);
std::thread t(process_data, std::move(ptr));
t.join();
// 此时ptr已经是nullptr
}
Lambda表达式是现代C++中创建线程最常用的方式,它允许我们直接在创建线程的地方定义线程逻辑:
cpp复制int main() {
int local_var = 100;
// 按值捕获
std::thread t1([local_var]() {
std::cout << "Value capture: " << local_var << std::endl;
});
// 按引用捕获
std::thread t2([&local_var]() {
local_var *= 2;
std::cout << "Reference capture: " << local_var << std::endl;
});
t1.join();
t2.join();
std::cout << "Final value: " << local_var << std::endl;
}
重要提示:引用捕获时要特别注意变量的生命周期。如果主线程中的变量在线程访问前就被销毁,会导致未定义行为。
std::thread是可移动但不可复制的类型,这意味着线程的所有权可以在不同对象间转移:
cpp复制void worker_task() {
std::cout << "Working..." << std::endl;
}
int main() {
std::thread t1; // 默认构造,不关联任何线程
std::thread t2(worker_task);
// 所有权转移
t1 = std::move(t2);
if (t2.joinable()) {
t2.join(); // 不会执行,t2已经不再拥有线程
}
t1.join();
}
这种特性特别适合将线程存入容器:
cpp复制std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
workers.emplace_back([](){
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker " << std::this_thread::get_id() << " completed\n";
});
}
for (auto& t : workers) {
t.join();
}
当多个线程访问共享数据时,必须使用同步机制防止数据竞争。std::mutex是最基本的同步原语:
cpp复制std::mutex mtx;
int shared_data = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock();
++shared_data;
mtx.unlock();
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final value: " << shared_data << std::endl;
}
为了避免忘记解锁,应该使用std::lock_guard或std::unique_lock:
cpp复制void safer_increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++shared_data;
} // 自动释放锁
}
当需要锁定多个互斥量时,使用std::lock可以避免死锁:
cpp复制std::mutex mtx1, mtx2;
void process() {
std::lock(mtx1, mtx2); // 同时锁定两个互斥量
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 安全地访问两个互斥量保护的数据
}
thread_local关键字允许每个线程拥有变量的独立实例:
cpp复制thread_local int thread_specific = 0;
void print_id() {
++thread_specific;
std::cout << "Thread " << std::this_thread::get_id()
<< ": " << thread_specific << std::endl;
}
int main() {
std::thread t1(print_id); t1.join();
std::thread t2(print_id); t2.join();
print_id();
// 输出示例:
// Thread 140737345967872: 1
// Thread 140737337575168: 1
// Thread 140737353360576: 1
}
这种机制非常适合需要维护线程特定状态的场景,如随机数生成器、数据库连接等。
虽然标准库没有直接提供线程池,但我们可以基于std::thread实现基本版本:
cpp复制class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex queue_mutex;
std::condition_variable condition;
bool stop = false;
public:
ThreadPool(size_t threads) {
for(size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while(true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(queue_mutex);
condition.wait(lock, [this] {
return stop || !tasks.empty();
});
if(stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
template<class F>
void enqueue(F&& f) {
{
std::unique_lock<std::mutex> lock(queue_mutex);
tasks.emplace(std::forward<F>(f));
}
condition.notify_one();
}
~ThreadPool() {
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
}
condition.notify_all();
for(std::thread &worker : workers)
worker.join();
}
};
std::thread::hardware_concurrency()可以获取硬件支持的并发线程数:
cpp复制unsigned int n = std::thread::hardware_concurrency();
std::cout << "This machine supports " << n << " concurrent threads.\n";
最佳实践:通常线程池大小设置为硬件并发数或稍多一些,但过多线程会导致上下文切换开销增加。
大多数标准库函数不是线程安全的,特别是涉及I/O的操作。例如,多个线程同时向cout输出可能导致输出混乱:
cpp复制std::mutex cout_mutex;
void safe_print(const std::string& msg) {
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << msg << std::endl;
}
线程函数中未捕获的异常会导致程序终止。可以通过包装函数捕获异常:
cpp复制void thread_function() {
try {
// 可能抛出异常的代码
} catch(const std::exception& e) {
std::cerr << "Thread failed: " << e.what() << std::endl;
}
}
int main() {
std::thread t(thread_function);
t.join();
}
调试多线程程序时,常用工具包括:
虽然本文主要讨论C++11的线程支持,但值得注意C++20引入的一些改进:
cpp复制// C++20示例:自动join的jthread
std::jthread worker([](std::stop_token stoken) {
while(!stoken.stop_requested()) {
std::cout << "Working...\n";
std::this_thread::sleep_for(1s);
}
std::cout << "Thread stopped cleanly\n";
});
std::this_thread::sleep_for(3s);
worker.request_stop(); // 优雅停止线程
在实际项目中,我发现合理使用std::thread的关键在于平衡并发度和代码复杂度。过度使用线程可能导致难以调试的问题,而适度使用可以显著提升性能。对于IO密集型任务,异步IO结合线程池通常是更好的选择。