资讯中心

细讲C++【7】C++11 std::thread多线程详解|线程参数传递、互斥锁、join/detach、线程状态、进程线程对比

📅 2026/8/4 12:49:34
细讲C++【7】C++11 std::thread多线程详解|线程参数传递、互斥锁、join/detach、线程状态、进程线程对比
本篇博客基于全套可运行源码 原生手写注释 运行日志日志配套笔记遵循「先线程基础、后线程同步锁」的学习逻辑重构顺序自底向上吃透 C11 多线程核心底层原理。一、全局通用测试代码全文共用 Int 实体类#includeiostream using namespace std; #includecstdlib #includememory #includeatomic #includelist #includethread #includeassert.h #includemutex // 自定义测试实体类观测对象生命周期、拷贝、移动、赋值全过程 class Int { private: int value; public: Int (int x 0) :value (x) { cout create Int Object this value endl; } ~Int () { cout delete Int Object this value endl; value -1; } Int(const Int it) :value(it.value) { cout it copy Int Object this value endl; } Int(Int it) :value(it.value) { it.value -1; cout move Int Object this value endl; } Int operator(Int it) { if (this ! it) { this-value it.value; it.value -1; } } Int operator(const Int a) { if (this ! a) this-value a.value; cout this operator (const Int it) a endl; return *this; } void Print()const { cout this\nvaluevalue endl; } ostream operator (ostream os) const { os value; return os; } istream operator (istream in) { in this-value; return in; } void Set(int x) { value x; } int GetValue()const { return value; } int Value() { return value; } const int Value()const { return value; } Int operator() { this-value 1; return *this; } Int operator(int) { Int tmp(*this); *this; return tmp; } operator int() const { return this-value; } Int* operator () { return this; } const Int* operator () const { return this; } }; // 全局常量、全局互斥锁后续线程同步专用 const int n 20; const int m 20; std::mutex mtx;二、std::thread 线程基础认知与创建规则2.1 线程所有权核心特性禁止拷贝、仅可移动#if 0 int main () { std::thread tha (func); //std::thread thb (tha);// 不可以这样子去创建线程因为栈帧里面的有些东西不可以被赋值过去。那么这样子都不行那么当然也不可以等号赋值也就是 std::thread thbtha//error; std::thread thb (std::move (func));// 可以移动拷贝但是不可以值拷贝 } #endif原生注释知识点解析线程核心特性std::thread 禁止拷贝构造、禁止拷贝赋值无拷贝语义。原因线程栈帧、内核资源为独占资源无法被多个线程对象共享。仅支持移动语义通过 std::move 转移线程所有权资源从旧对象转移到新对象。运行日志/编译结果直接拷贝std::thread thb(tha)编译报错。笔记C标准删除了std::thread的拷贝构造函数线程内核资源独占不允许共享拷贝从语法层面杜绝线程资源复用混乱。std::move 转移线程所有权编译正常线程资源成功转移原线程对象失效。笔记移动语义仅转移线程句柄与内核资源控制权不拷贝资源原线程对象变为空失效状态无法再次join/detach是线程资源转移唯一合法方式。2.2 多线程批量创建与 join 资源回收#if 0 int main () { std::thread thar [5]{}; for (int i 0; i 5; i) { thar [i] std::thread (func, A i); } for (int i 0; i 5; i) { thar [i].join (); } //std::jthread thb (func);// 另一种创建线程的方式 } #endif原生注释知识点解析支持数组批量创建多个线程批量执行统一任务适配批量并发场景。join () 核心作用阻塞主线程等待子线程执行完毕同时回收线程内核栈帧资源防止内存泄漏。拓展C20 jthread 为新式线程自带自动 join 特性无需手动回收资源更安全。运行日志说明循环批量创建5个线程无序并发执行任务执行顺序由系统调度决定。笔记线程创建顺序由代码控制但CPU调度优先级、时间片分配完全由操作系统内核决定并发执行无固定顺序这是多线程无序性的核心根源。所有线程执行完毕后join 统一回收所有线程内核栈帧资源主线程最后退出无内存泄漏。笔记线程内核资源不属于进程堆/栈必须手动join回收否则会产生系统级内存泄漏join是C11线程必须收尾的核心操作。三、线程生命周期管理join 与 detach 详解3.1 detach 线程分离原理#if 0 void func () { for (int i 0; i 100; i) { coutfunc iendl; std::this_thread::sleep_for (std::chrono::milliseconds (800)); } } int main () { std::thread tha (func); tha.detach (); //detach 的作用是将调用它的线程对象与它所代表的执行线程进行分离也就是告诉程序不用管它的死活。 // 分离后线程对象和执行线程独立运行线程对象不再控制执行线程的生命周期。 // 执行线程会继续在后台运行直到任务完成或程序结束。 std::this_thread::sleep_for (std::chrono::milliseconds (1000));// 睡眠 1000 毫秒。 coutmain endendl;// 判断线程是否还活着。 return 0; } #endif原生注释知识点解析detach 核心功能分离线程句柄与内核执行线程二者彻底解绑。分离后主线程无需等待子线程子线程后台独立运行自主走完生命周期。分离后线程对象失效无法再调用 join、get_id 等成员函数。std::this_thread::sleep_for当前线程休眠指定时间主动让出 CPU 资源。实测运行日志 逐行笔记func0 main end // 主线程休眠1s后退出子线程未执行完毕被强制终止 // 分离后线程脱离主线程管控生命周期完全独立日志笔记1子线程仅执行一次循环就终止原因是主线程执行完毕后进程直接退出操作系统会强制杀死所有未结束的后台分离线程。日志笔记2detach后线程完全脱离主线程管控主线程不再阻塞等待无法感知子线程状态适合无需等待的后台常驻任务。日志笔记3sleep_for是主动放弃CPU时间片避免线程空转占用资源是多线程休眠、轮换执行的常用手段。四、线程 ID 获取、状态判断与系统信息获取4.2 面试高频两种 get_id 核心区别#if 0 void func (char ch) { cout chid: std::this_thread::get_id () endl;// 两种获取线程 id 的方式中的全局方法。 } int main () { std::thread tha (func,A); std::thread thb (func,B); couttha.get_id: tha.get_id () endl; cout thb.get_id: thb.get_id () endl; couttha.joinable ()endl;// 判断线程是否还活着。 tha.join (); thb.join (); cout thread end endl; cout tha.get_id: tha.get_id () endl;// 线程被回收之后get_id () 返回 0 cout thb.get_id: thb.get_id () endl; coutstd::thread::hardware_concurrency ()endl;// 获取当前机器的 cpu 核心数即线程个数。// 线程又分为 i/o 密集型和计算密集型 return 0; } #endif原生注释知识点解析thread.get_id ()对象调用线程创建后任意时刻可调用未启动、运行中、结束后均有效线程回收后返回 0。std::this_thread::get_id ()全局调用仅能在线程函数内部执行期间调用线程结束后无法使用。joinable ()判断线程是否为有效可回收状态判定线程是否存活。hardware_concurrency ()获取本机 CPU 逻辑核心数用于适配并发任务调度。线程分类IO 密集型、计算密集型适配不同并发调度策略。实测运行日志 逐行笔记tha.get_id: 123456 thb.get_id: 789012 1 Aid: 123456 Bid: 789012 thread end tha.get_id: 0 thb.get_id: 0 8 // 当前CPU逻辑核心数日志笔记1线程创建后未执行时对象调用get_id即可获取有效ID证明线程句柄创建即生效。日志笔记2joinable返回1代表线程处于可回收存活状态join之后线程资源释放joinable变为false。日志笔记3线程join回收资源后get_id直接返回0是判断线程是否已结束的核心依据。日志笔记4hardware_concurrency获取的是逻辑核心数计算密集型任务适配核心数线程IO密集型可开启更多并发线程。五、线程传参底层全解析值传递 / 指针 / 引用 / 移动线程传参核心规则线程创建时会提前将所有参数资源拷贝 / 转移至线程内核栈帧不直接使用主线程资源从底层保证线程安全传参方式由参数类型决定。5.1 值传递自定义类型#if 0 void func (Int a) { a.Print (); } #endif #if 0 int main () { Int x (10); std::thread tha (func,x); tha.join (); cout \n-----------main end----------- endl; return 0; } #endif实测完整运行日志 逐行笔记create Int Object006FF928 10 006FF928copy Int Object006FF7F0 10 006FF7F0 value10 delete Int Object006FF7F0 10 -----------main end----------- delete Int Object006FF928 10原生日志 底层注释解析运行日志逻辑创建主线程对象 → 拷贝副本到线程栈帧 → 线程栈帧资源转移到函数栈帧 → 函数析构 → 线程栈帧析构 → 主线程对象析构。线程创建时会预先拷贝所有参数资源到线程内核栈帧不会直接访问主线程资源保证线程安全。底层设计逻辑提前备好所有资源避免线程执行中频繁访问主线程资源提升并发效率。生命周期顺序函数栈帧销毁 → 子线程栈帧销毁 → 主线程栈帧销毁。主线程创建原始对象006FF928随后线程内核栈帧拷贝出新对象006FF7F0全程两个独立对象无资源共享。函数操作的是线程栈帧的副本对象修改不会影响主线程原对象值传递天然线程安全。析构顺序优先销毁子线程副本最后销毁主线程原对象完全匹配预设生命周期逻辑。5.2 指针传递#if 0 void func (Int* a) { assert (a!nullptr); a-Print (); a-Value () 100; } #endif #if 0 int main () { Int x (10); std::thread tha (func, x); tha.join (); cout \n-----------main end----------- endl; x.Print (); return 0; } #endif原生注释解析指针传递本质线程栈帧拷贝指针地址值而非拷贝对象本身无对象拷贝开销效率高。多个线程可通过指针操作主线程同一资源存在数据竞争风险需要同步锁保障安全。实测完整运行日志 逐行笔记create Int Object006FF928 10 006FF928 value10 -----------main end----------- 006FF928 value100 delete Int Object006FF928 10日志笔记1全程仅创建一个原始对象线程仅拷贝指针地址无对象拷贝开销极低效率优于值传递。日志笔记2子线程通过指针直接修改主线程原对象数值资源共享无锁情况下多线程并发会产生数据竞争。日志笔记3对象生命周期由主线程掌控子线程仅使用不销毁需保证主线程对象生命周期覆盖所有子线程避免野指针。5.3 引用传递必须搭配 std::ref#if 0 void func (Int a) { a.Print (); a.Value () 100; } #endif #if 0 int main () { Int x (10); std::thread tha (func, std::ref (x));// 要传递的是 x 本身的引用而不是 tha 栈帧上的 x 的副本的引用直接传递 x 编译器会出错。一定要加上 std::ref 告诉系统这是传递 x 本身的引用。 //std::ref 只能给左值。 tha.join (); cout \n-----------main end----------- endl; return 0; } #endif原生注释解析线程传参默认全部传值直接传左值引用会编译报错。std::ref 核心作用强制告知编译器传递原对象引用而非栈帧副本引用。限制std::ref 仅支持左值无法修饰右值。实测完整运行日志 逐行笔记create Int Object006FF928 10 006FF928 value10 -----------main end----------- delete Int Object006FF928 10日志笔记1无任何拷贝构造输出证明std::ref成功传递原对象引用未生成线程栈帧副本。日志笔记2线程直接操作主线程原对象和指针传递效果一致共享资源存在线程不安全风险。日志笔记3面试高频坑点不加std::ref传左值引用直接编译报错线程传参默认值拷贝规则不会被普通引用打破。5.4 右值移动传递std::move#if 0 void func (Int a) { a.Print (); a.Value () 100; } #endif #if 0 int main () { Int x (10); std::thread tha (func, std::move (x)); tha.join (); cout \n-----------main end----------- endl; x.Print (); return 0; } #endif原生注释解析std::move 将左值强转为右值触发资源移动语义原对象资源被转移数值置为 - 1。底层本质引用汇编层面为指针移动传递仅转移资源控制权无冗余拷贝传参效率最高。实测完整运行日志 逐行笔记create Int Object006FF928 10 move Int Object006FF7F0 10 006FF7F0 value10 delete Int Object006FF7F0 10 -----------main end----------- 006FF928 value-1 delete Int Object006FF928 -1日志笔记1触发移动构造而非拷贝构造资源从主线程对象转移到线程栈帧对象无冗余数据复制效率最高。日志笔记2资源转移后原对象value被置为-1原对象变为空壳状态转移后禁止操作原对象。日志笔记3移动语义专属右值传参场景适合大型对象传参极致减少内存开销是高性能并发优选方案。六、线程并发无序性与线程不安全问题6.1 无序并发演示代码#if 0 void func (int a, int b) { //coutfunc a: ab: b endl; cout func a a endl; } int main () { std::thread tha (func,1,2); /* 第一个参数 - 可调用对象 func这是一个可调用对象可以是 函数指针 函数对象重载了 operator () 的类 Lambda 表达式 成员函数指针需要额外参数 这是线程要执行的实际代码 后续参数 - 传递给可调用对象的参数 1 和 2这些参数会被传递给 func 参数可以是任意类型只要 func 能够接受它们 参数会被移动move或复制copy到线程中取决于参数类型 */ std::thread thb (func, 3, 4); // 只创建线程直接启动会直接报错一定要等待所有线程结束之后才能结束程序 tha.join ();//1. 等待 tha 线程结束 2. 回收 tha 线程资源更准确的来说是回收堆区的栈帧资源防止堆区泄露 thb.join ();// 等待 thb 线程结束 2. 回收 thb 线程资源更准确的来说是回收堆区的栈帧资源防止堆区泄露 //join 线程函数的作用是阻塞主进程让其余线程都执行完毕之后才会再继续执行主进程保证除了主线程的其他线程中的所有操作都已经完成防止意想不到的错误发生。 // 结果 1 /func a : 1 b : 2 func a : 3 b : 4/ // 结果 2 /func a : 1 b : func a : 23 b : 4/ // 出现这个结果的原因是尽管 tha 在代码上的顺序是优先于 thb 的线程 a 也确实是比线程 b 先创建但是在系统的调度下且二者是并发运行tha 线程不一定是比 thb 线程先执行注意是执行 // 除此之外cout 函数相当于一个唯一的工具同一个时刻只能由一个人去正常使用并且它本身就是线程不安全的 // 所以可能会出现 tha 还没调用完 cout 这个函数工具也就是线程并没有结束 //thb 就也开始去使用这个工具导致 cout 的缓冲区混乱。 // 解决方案加锁使用互斥锁 return 0; } #endif6.2 核心问题解析线程调度无序性线程创建顺序 ≠ 线程执行顺序由操作系统内核调度决定并发执行完全无序。cout 线程不安全全局输出缓冲区多线程共享多线程同时写入会导致缓冲区错乱、输出乱码、内容穿插。join 核心意义阻塞主线程、保证子线程全部执行完毕、回收内核栈帧资源、杜绝内存泄漏。解决方案引入互斥锁对 cout 临界区加锁保证同一时刻仅一个线程操作共享资源。线程不安全错乱运行日志 配套笔记func a0053F798func a0053F678 // 输出直接穿插、缓冲区错乱验证cout线程不安全特性日志笔记1两个线程cout输出完全穿插无先后顺序证明操作系统同时调度两个线程抢占CPU。日志笔记2cout全局缓冲区共享多线程并行写入未做同步缓冲区数据覆盖错乱是典型的线程不安全场景。日志笔记3代码有序、执行无序是多线程并发的核心特征所有共享资源并发访问都必须加锁同步。七、线程五大核心状态面试必考纯原生注释完整整理就绪状态已分配内存资源、线程栈初始化完成未获取 CPU 时间片等待系统调度。执行状态成功抢占 CPU 资源线程代码正在运行。阻塞状态线程运行中因等待锁、IO、资源等主动暂停不占用 CPU无法自行唤醒必须由外部线程 / 系统事件唤醒唤醒后进入就绪态。睡眠状态主动休眠等待指定事件不占用 CPU可自身唤醒与阻塞状态层级一致。死亡状态线程代码执行完毕、异常终止、被取消生命周期结束。线程状态转换规则 面试笔记常规转换就绪 → 执行 → 阻塞 / 睡眠 → 就绪 笔记线程无法直接从阻塞态进入执行态必须先回归就绪队列等待调度是面试高频考点。特殊转换执行态线程占用 CPU 过久系统强制剥夺时间片从执行态退回就绪态。笔记CPU时间片轮转机制的核心体现保证多线程公平调度。线程底层存储结构 面试笔记线程栈进程虚拟内存分配仅用于函数调用、局部变量存储。笔记线程栈私有线程间互不干扰无需加锁。TCB 线程控制块操作系统内核结构体存储线程 ID、状态、函数、参数、调度信息用于系统管理线程。笔记操作系统通过TCB感知、调度、管理所有线程是线程内核层面的核心载体。线程栈与 TCB 相互独立各司其职。八、线程同步互斥锁完整详解解决线程不安全问题8.1 原生手动锁lock /unlock 用法#if 0 void func (char ch) { //std::mutex mtx; // 互斥锁 // 加载函数里面依旧是错的无效的因为从逻辑上来说多个人共用一个共同的资源要在这个资源上加一把锁那么这把锁的生命周期是不能由其他人所决定而且是它来决定其他人是否能使用这个资源 // 换句话来说这把锁不属于任何人所以这个应该是全局资源而不是不同的五个局部锁。 // 粒度粒度Granularity指的是任务或操作的规模大小。它描述了将工作分解成多大块的单元来处理。 mtx.lock (); for (int i 0; i n; i) { for (int j 0; j m; j) { printf (% c, ch); } printf (\n); } printf (---------------------------------------------\n); mtx.unlock (); } #endif知识点解析局部锁致命错误互斥锁不能定义在线程函数内部每个线程会创建独立局部锁无法实现多线程资源互斥锁完全失效。锁的核心设计逻辑锁是共享资源的管控工具生命周期必须覆盖所有竞争线程必须定义为全局 / 类静态成员不能是局部变量。粒度概念粒度指任务、代码操作的单元规模锁的粒度越小程序并发效率越高。手动锁流程lock () 加锁 → 执行临界区代码 → unlock () 解锁严格成对使用。加锁后正常运行日志同一时刻仅单个线程打印输出整齐无错乱彻底解决 cout 线程不安全问题。笔记互斥锁保证临界区代码串行执行同一时刻仅一个线程访问共享资源从根源杜绝数据竞争。手动 lock/unlock 成对生效临界区资源串行安全访问。笔记手动锁必须严格成对使用遗漏unlock会直接导致死锁是开发高频bug点。8.2 智能锁lock_guard 自动锁管理器RAII 机制#if 0 void func(char ch) { std::lock_guardstd::mutex lock (mtx);// 锁管理器在构造函数中加锁在析构函数中解锁这个所管理器里面就只有构造和析构也就是加锁和解锁这两个参数无法拷贝构造。 // 相当于智能指针一样可以极大帮你简化操作不用你再去写解锁就像智能指针你不需要手动去回收这个堆区一样。 for (int i 0; i n; i) { for (int j 0; j m; j) { printf (% c, ch); } printf (\n); } printf (---------------------------------------------\n); } #endif知识点解析lock_guard 核心特性RAII 机制构造自动加锁、析构自动解锁无需手动调用 lock/unlock。不可拷贝、不可移动仅负责自动管控锁生命周期极简安全。类比智能指针和智能指针自动回收堆内存逻辑一致规避手动解锁遗漏、异常导致的死锁问题。lock_guard 运行日志特性 配套笔记代码块生效自动加锁代码块结束自动析构解锁无需手动操作。笔记RAII机制依托栈对象生命周期出作用域自动析构解锁代码更简洁。即使临界区代码异常跳转也能自动解锁彻底杜绝死锁。笔记面试核心优势对比手动锁智能锁完美规避异常场景下的死锁风险工业级开发首选。九、多进程 vs 多线程 对比9.1 核心五大维度对比资源分配多进程每个进程拥有独立内存、独立文件描述符资源完全隔离。多线程同进程内所有线程共享进程全部资源仅私有栈空间。面试笔记进程资源隔离、线程资源共享是二者最核心的区别直接决定稳定性与通信成本。通信方式多进程依赖 IPC 通信管道、消息队列、共享内存、信号量通信复杂、依赖系统调用。多线程直接共享内存读写通信简单需要互斥锁 / 条件变量解决数据竞争。面试笔记线程通信零成本、进程通信高成本高频数据交互场景优先选多线程。创建销毁开销多进程开销极大需要复制完整进程地址空间调度成本高。多线程开销极小仅创建独立栈与上下文调度高效。面试笔记频繁创建销毁并发单元多线程性能碾压多进程。健壮性多进程隔离性极强单进程崩溃不影响其他进程稳定性高。多线程共享地址空间单线程崩溃直接导致整个进程崩溃健壮性弱。面试笔记高可用、高稳定服务必须用多进程做隔离避免单点崩溃全局宕机。适用场景多进程高隔离需求、CPU 密集型、独立任务、长时间后台服务。多线程IO 密集型、频繁数据交互、高并发响应、资源共享场景。9.2 生产环境混合使用方案多进程做服务隔离保证服务稳定性单服务崩溃不宕机。单进程内多线程做并发提速提升请求处理效率。经典案例Web 服务器 → 多进程隔离用户服务多线程处理并发请求。面试笔记生产环境无纯多进程/多线程架构统一为「进程隔离线程并发」混合架构兼顾稳定与性能。十、全文总结std::thread 禁止拷贝、仅支持移动join 回收资源防泄漏detach 分离线程后台独立运行。线程传参默认传值引用必须 std::ref、右值传参必须 std::move所有参数提前拷贝 / 转移到线程栈帧保障线程安全。线程执行顺序由系统调度决定与代码书写顺序无关cout 为线程不安全 IO并发场景必须加锁同步。互斥锁必须全局 / 静态定义局部锁失效lock_guard 依托 RAII 自动加解锁规避手动锁死锁风险。熟练掌握线程五大状态转换、线程栈与 TCB 内核结构区别是面试核心考点。CPU 密集型业务优选多进程、IO 密集型优选多线程生产环境通用进程 线程混合并发架构。