c++_std::thread多线程

📅 2026/8/19 14:28:31
c++_std::thread多线程
文章目录c_std::thread多线程std::thread介绍创建与启动线程准备启动线程的核心机制可调用对象通常有以下四种形式向线程传递参数传递方式案例案例解析std::thread生命周期管理使用案例std::thread的数据竞争典型的数据竞争示例解决数据竞争方法容器操作的数据竞争陷阱c_std::thread多线程std::thread介绍std::thread是创建和管理线程的核心类。它极大地简化了多线程编程使得开发者无需再依赖特定操作系统的API如 POSIX 的 pthread 或 Windows 的 CreateThread即可实现跨平台的并发编程。创建与启动线程准备使用std::thread需要包含thread头文件。#includethread启动线程的核心机制要启动一个线程只需创建一个std::thread对象并将可调用对象Callable Object传递给它的构造函数。一旦对象被构造新线程就会立即在后台开始执行无需显式调用类似start()的方法。可调用对象通常有以下四种形式1. 普通函数指针将普通函数的名称作为参数传入线程启动后会执行该函数。voidtask(){// 线程执行的代码}std::threadt(task);// 启动线程执行 task2. Lambda 表达式最推荐的现代 C 写法语法简洁且可以直接捕获外部变量。std::threadt([](){// 线程执行的代码});3. 函数对象仿函数通过重载类的operator()运算符将类的实例作为可调用对象传入。适用于需要携带状态的线程任务。classTask{public:voidoperator()(){// 线程执行的代码}};Task myTask;std::threadt(myTask);4. 类的成员函数启动非静态成员函数时必须额外传递该类的对象指针或引用作为第二个参数静态成员函数则与普通函数用法相同。classWorker{public:voiddoWork(){/* 非静态成员函数 */}staticvoidstaticWork(){/* 静态成员函数 */}};Worker w;std::threadt1(Worker::doWork,w);// 传入对象指针std::threadt2(Worker::staticWork);// 静态成员函数直接传入向线程传递参数传递方式创建线程时可以在可调用对象之后继续传递参数这些参数会被自动转发给线程函数。默认按值传递拷贝为了保证线程安全std::thread默认会将参数拷贝到线程内部的私有栈中。传递引用如果需要在线程中修改外部变量必须使用std::ref()或std::cref()进行包装否则编译器会将其隐式拷贝导致修改无法反映到外部变量。转移所有权如果参数是不可拷贝的资源如std::unique_ptr可以使用std::move()将资源的所有权移交给线程。案例#includethread#includeiostream#includememoryusingnamespacestd;// 线程执行的函数接收一个引用和一个智能指针voidprocess(intval,std::unique_ptrintptr){std::cout【子线程】进入 process 函数std::endl;// 1. 通过引用修改外部的变量val;std::cout【子线程】val 修改后的值为: valstd::endl;// 2. 使用转移过来的智能指针std::cout【子线程】ptr 指向的值为: *ptrstd::endl;std::cout【子线程】process 函数执行完毕std::endl;}intmain(){intx10;autoptrstd::make_uniqueint(100);std::cout【主线程】启动线程前x 的值为: xstd::endl;// 启动线程// std::ref(x) 告诉编译器把 x 的引用传给子线程而不是拷贝一份// std::move(ptr) 告诉编译器把 ptr 的所有权转移给子线程因为 unique_ptr 不能拷贝std::threadt(process,std::ref(x),std::move(ptr));// 主线程等待子线程执行完毕t.join();// 验证子线程的修改是否生效std::cout【主线程】子线程结束后x 的值变为: xstd::endl;return0;}案例解析为什么用 std::ref(x)如果你直接写std::thread t(process, x, ...)std::thread会默认把x拷贝一份传给子线程。子线程里修改的是拷贝出来的副本主线程里的x依然是10。使用std::ref(x)才能真正做到“跨线程共享同一个变量”。为什么用 std::move(ptr)std::unique_ptr的设计原则是“独占所有权”它禁止被拷贝。如果不加std::move编译器会直接报错。加上std::move后主线程放弃了对这块内存的控制权将其“移交”给了子线程。std::thread生命周期管理线程一旦启动就必须在std::thread对象销毁前明确其生命周期否则程序会直接调用std::terminate()崩溃。必须选择以下两种方式之一join()阻塞等待主线程在此处阻塞直到子线程执行完毕。适用于需要等待子线程结果的场景。detach()分离线程将子线程与主线程解绑使其成为后台守护线程Daemon Thread。主线程不再关心它的执行状态线程结束后由操作系统自动回收资源。std::threadt(task);t.join();// 正确等待线程结束// 或者t.detach();// 正确分离线程后台运行绝对禁止在对象析构前既不调用join()也不调用detach()或者对同一个线程重复调用这两个方法。使用案例#includethread#includeiostream#includememoryusingnamespacestd;// 定义第一个线程执行的函数voidThreadFUnc1(){coutThreadFunc1()---A\nendl;// 让当前线程休眠 2 秒模拟耗时操作this_thread::sleep_for(chrono::seconds(2));coutThreadFunc1()---Bendl;}// 定义第二个线程执行的函数接收两个参数voidThreadFUnc2(intargs,string sp){coutThreadFunc2()---A\nendl;// 让当前线程休眠 7 秒模拟更长的耗时操作this_thread::sleep_for(chrono::seconds(7));coutThreadFunc2()---Bendl;}intmain(){// 创建并启动线程1传入函数 ThreadFUnc1threadths1(ThreadFUnc1);// 创建并启动线程2传入函数 ThreadFUnc2 及其参数 10 和 LSthreadths2(ThreadFUnc2,10,LS);// 【关键点1】主线程在此处阻塞等待直到 ths1 执行完毕约等待2秒ths1.join();coutjoinendl;// 【关键点2】将 ths2 与主线程分离。分离后ths2 在后台独立运行// 主线程不再关心它的执行状态也不会等待它。ths2.detach();coutdetachendl;// 【核心原因】主线程执行到这里后main 函数结束整个程序进程随之销毁。// 此时 ths2 内部的 7 秒休眠可能还没结束更不用说执行后面的打印了。// 操作系统会强制回收所有资源导致 ths2 被强行终止所以 ---B 无法打印。return0;}std::thread的数据竞争在 C 多线程编程中数据竞争Data Race是最核心、也是最危险的并发问题。根据 C 标准定义当两个或多个线程同时访问同一个内存位置且其中至少有一个是写操作同时这些访问又没有通过任何同步机制如互斥锁或原子操作进行排序时就会发生数据竞争。核心警告一旦发生数据竞争程序的行为是未定义的Undefined Behavior。这意味着程序可能会崩溃、得出错误的计算结果甚至在某些测试中看起来正常但在生产环境中引发灾难性故障。典型的数据竞争示例最经典的数据竞争场景多个线程同时对全局变量 counter 进行自增操作。#includeiostream#includethreadintcounter0;// 共享变量voidincrement(){for(inti0;i100000;i){counter;// 危险非原子操作存在数据竞争}}intmain(){std::threadt1(increment);std::threadt2(increment);t1.join();t2.join();// 期望结果是 200000但实际运行结果可能小于 200000std::cout计数器值: counterstd::endl;return0;}解决数据竞争方法要消除数据竞争必须保证对共享内存的访问是安全的。C 提供了以下两种主要解决方案方案 A使用互斥锁std::mutex互斥锁是最常用的同步工具。通过std::lock_guardRAII 风格可以确保同一时刻只有一个线程能修改共享数据。#includeiostream#includethread#includemutexintcounter0;// 共享变量std::mutex mtx;// 声明互斥锁voidsafeIncrement(){for(inti0;i100000;i){std::lock_guardstd::mutexlock(mtx);// 自动加锁离开作用域自动解锁counter;}}intmain(){std::threadt1(safeIncrement);std::threadt2(safeIncrement);t1.join();t2.join();std::cout计数器值: counterstd::endl;return0;}方案 B使用原子操作std::atomic对于简单的计数器或状态标志使用原子类型是更高效的选择。它能在硬件层面保证读-改-写操作的不可分割性。#includeiostream#includethread#includemutex#includeatomicstd::atomicintatomicCounter(0);// 声明原子变量voidatomicIncrement(){for(inti0;i100000;i){atomicCounter;// 线程安全的原子操作}}intmain(){std::threadt1(atomicIncrement);std::threadt2(atomicIncrement);t1.join();t2.join();std::cout计数器值: atomicCounterstd::endl;return0;}容器操作的数据竞争陷阱不仅是基本数据类型标准库容器在多线程下也有严格的限制安全多线程同时读写容器中不同元素的内容是安全的例如 vec[0] 1; 和 vec[1] 2; 在不同线程中。不安全多线程同时修改同一个元素或者涉及容器大小的改变如 push_back、erase都会引发数据竞争。特例std::vectorbool是例外由于它内部使用了位压缩即使修改不同索引的元素也可能发生数据竞争应尽量避免在多线程中使用。