【Linux】二十六.线程篇三《Linux多线程编程完全指南:线程控制、地址空间与__thread详解》

发布时间:2026/8/3 0:15:21
【Linux】二十六.线程篇三《Linux多线程编程完全指南:线程控制、地址空间与__thread详解》 在上节内容中我们学写了线程的创建相关的知识点今天我们紧接着学习后面的知识一.线程的控制1.线程终止如果需要只终⽌某个线程⽽不终⽌整个进程,可以有三种⽅法:1. 从线程函数return。这种⽅法对主线程不适⽤,从main函数return相当于调⽤exit。2. 线程可以调⽤pthread_ exit终⽌⾃⼰。3. ⼀个线程可以调⽤pthread_ cancel终⽌同⼀进程中的另⼀个线程。pthread_exit函数功能线程终⽌ 原型: void pthread_exit(void *value_ptr); 参数: value_ptr:value_ptr不要指向⼀个局部变量。 返回值 ⽆返回值跟进程⼀样线程结束的时候⽆法返回到它的调⽤者⾃⾝接口需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是⽤malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。a.return返回代码如下#include iostream #include pthread.h #include unistd.h using namespace std; void* thread_func(void* args) { // 线程要做的事情打印5次 for (int i 0; i 5; i) { cout 子线程: i endl; sleep(1); } // 线程结束返回一个数字假装是结果 return (void*)100; } int main() { pthread_t tid; // 创建线程 pthread_create(tid, nullptr, thread_func, nullptr); cout 主线程: 等待子线程结束... endl; // 等待子线程结束接收返回值 void* ret nullptr; pthread_join(tid, ret); cout 主线程: 子线程结束了返回了 (long long)ret endl; return 0; }这个程序是创建了一个子线程子线程里面从0打印到4每次打印完就睡1秒一共打印5次然后return 100子线程结束。主线程在子线程运行的时候用pthread_join等着等子线程结束了拿到100打印出来。thread_func是子线程的函数参数是void* args但是这个参数没用到所以创建线程的时候传了nullptr。函数里面有一个for循环int i0; i5; i每次打印i然后sleep(1)。循环结束就return (void*)100。main里面定义pthread_t tid然后pthread_create第一个参数是tid第二个nullptr第三个thread_func第四个nullptr。然后cout打印主线程: 等待子线程结束...。然后void* ret nullptrpthread_join(tid, ret)主线程就卡在这等子线程结束。等到了之后打印主线程: 子线程返回了 然后把ret转成long long打印出来就是100。最后return 0。这个代码的关键就是pthread_create创建线程pthread_join等待线程结束并接收返回值return让线程结束并返回数据。b.用pthread_exit退出#include iostream #include pthread.h #include unistd.h using namespace std; void* thread_func(void* args) { for (int i 0; i 3; i) { cout 子线程: i endl; sleep(1); } // 用 pthread_exit 退出返回 13 pthread_exit((void*)13); // 这行不会执行 cout 子线程: 我退出了 endl; } int main() { pthread_t tid; pthread_create(tid, nullptr, thread_func, nullptr); cout 主线程: 等待... endl; void* ret nullptr; pthread_join(tid, ret); cout 主线程: 子线程返回了 (long long)ret endl; return 0; }代码如下这个程序和代码一差不多区别是子线程只打印0、1、2一共三次然后调用pthread_exit((void*)13)结束线程返回13。pthread_exit后面的cout 子线程: 我退出了 endl不会执行因为线程已经结束了。thread_func里面for循环只到3i3所以打印0、1、2。循环结束后pthread_exit((void*)13)线程立刻结束后面的代码不跑了。main里面和代码一一样pthread_create创建线程pthread_join等待线程结束打印返回值。只是打印的时候ret是13。退出方式的区别// 方式1return推荐 return (void*)100; // 方式2pthread_exit在函数中间提前退出时用 pthread_exit((void*)13); // 两者效果一样都能让线程结束并返回数据pthread_exit和return的区别就是return只能写在函数最后pthread_exit可以写在函数中间任意位置写了之后线程立刻结束后面的都不执行。两个都能返回数据给主线程。c.pthread_cancel函数功能取消⼀个执⾏中的线程 原型: int pthread_cancel(pthread_t thread); 参数: thread:线程ID 返回值成功返回0失败返回错误码代码如下#include iostream #include pthread.h #include unistd.h using namespace std; void* threadRoutine(void* args) { cout 子线程开始执行 endl; sleep(1); while(true) { cout 子线程: (char*)args running... endl; sleep(1); } cout 子线程退出 endl; return nullptr; } int main() { pthread_t tid; cout 1. 主线程创建子线程 endl; pthread_create(tid, nullptr, threadRoutine, (void*)thread 1); sleep(2); cout 2. 主线程调用pthread_cancel取消子线程 endl; pthread_cancel(tid); int cnt 0; while(true) { cout 3. 主线程 running... endl; sleep(1); cnt; if(cnt 3) break; } void* ret nullptr; cout 4. 主线程调用pthread_join等待子线程结束 endl; pthread_join(tid, ret); cout 5. 主线程等待结束子线程退出码: (long long)ret endl; return 0; }pthread_cancel是取消线程的函数传入线程ID就能让那个线程终止。这个程序里主线程创建子线程后sleep了2秒让子线程先跑起来打印点东西然后调用pthread_cancel(tid)取消子线程。子线程收到取消请求后立刻结束while死循环被打断后面的代码都不执行了。pthread_cancel只是发送取消请求不会等线程真正结束。所以主线程调用完pthread_cancel后继续执行自己的事情打印三次running。后面调用pthread_join的时候才会真正等待子线程结束并回收资源。子线程被取消后pthread_join会立刻返回退出码是-1也就是PTHREAD_CANCELED。被取消的线程退出码固定是-1这个值定义在pthread.h里叫PTHREAD_CANCELED。pthread_join的第二个参数会收到这个值主线程就能知道子线程是被取消的而不是正常结束的。子线程的死循环本身不会结束如果没有pthread_cancel它会一直跑。主线程调用pthread_cancel后子线程就被强制终止了。所以运行结果里子线程只打印了开始信息和两次running被取消后就不再打印了。主线程最后拿到-1说明线程是被取消的。2.线程等待为什么需要线程等待已经退出的线程其空间没有被释放仍然在进程的地址空间内。创建新的线程不会复⽤刚才退出线程的地址空间。功能等待线程结束 原型 int pthread_join(pthread_t thread, void **value_ptr); 参数: thread:线程ID value_ptr:它指向⼀个指针后者指向线程的返回值 返回值成功返回0失败返回错误码接口调⽤该函数的线程将挂起等待,直到id为thread的线程终⽌。thread线程以不同的⽅法终⽌,通过pthread_join得到的终⽌状态是不同的总结如下:如果thread线程通过return返回,value_ ptr所指向的单元⾥存放的是thread线程函数的返回值。如果thread线程被别的线程调⽤pthread_ cancel异常终掉,value_ ptr所指向的单元⾥存放的是常数PTHREAD_ CANCELED。如果thread线程是⾃⼰调⽤pthread_exit终⽌的,value_ptr所指向的单元存放的是传给pthread_exit的参数。如果对thread线程的终⽌状态不感兴趣,可以传NULL给value_ ptr参数。线程在Linux内核里实际上是一个轻量级进程内核为每个线程维护一个task_struct结构体也就是通常说的PCB。这个结构体里包含了线程的所有信息包括状态、优先级、寄存器上下文、栈指针等等。线程退出的时候入口函数返回或者调用pthread_exit退出码会被保存到task_struct里的某个字段中。pthread库在用户态也维护了一个数据结构和内核的task_struct对应里面记录了线程的状态、线程ID、退出码等信息。线程退出时退出码先被写入用户态的这个结构里然后通过系统调用通知内核线程结束了。内核收到通知后把线程的task_struct标记为退出状态但不会立刻释放因为主线程可能还要通过pthread_join获取退出码。主线程调用pthread_join的时候通过线程ID找到对应的用户态数据结构检查线程是否已经结束。如果还没结束就阻塞等待内核把主线程挂起。等子线程真正退出后内核唤醒主线程pthread_join从用户态数据结构里读出退出码写到用户传入的void**指向的内存位置。然后pthread_join通知内核可以彻底回收这个线程的资源了内核释放task_struct和线程的栈空间完成回收。所以pthread_join做的事就是从PCB里读退出码然后释放PCB和线程占用的所有资源。如果线程结束但不调用pthread_joinPCB和栈空间就一直不会被释放造成资源泄漏。这种泄漏不会让进程立刻崩溃但会慢慢累积进程占用的内核资源越来越多最终可能无法创建新线程或者系统变慢。这就是必须用pthread_join等待的原因。代码如下#include iostream #include pthread.h #include unistd.h using namespace std; void* threadRoutine(void* args) { int* p new int(100); return (void*)p; // 返回值存入PCB } int main() { pthread_t tid; pthread_create(tid, nullptr, threadRoutine, nullptr); void* ret nullptr; pthread_join(tid, ret); // 从PCB里读退出码写入ret int* result (int*)ret; cout *result endl; delete result; return 0; }过程如下子线程在堆上new了一个int存100把地址返回给主线程。主线程用pthread_join等着拿这个地址然后打印100最后delete释放。关键点在于堆内存不会因为线程结束就销毁所以子线程返回堆地址是安全的。如果返回栈地址子线程一结束栈就没了主线程再访问就会出问题。pthread_join的第二个参数是void**传的是ret的地址这样函数内部才能修改ret的值把子线程返回的地址写进去。调用完pthread_join后ret就指向了子线程的堆内存。整个过程就是子线程造数据主线程接数据最后释放。3. 分离线程pthread_detach用于创建线程它是一个回调函数。如果线程创建成功则会执行 start_routine编译和链接时需要引入 pthread。接口int pthread_detach(pthread_t thread);参数thread是线程ID就是要分离的那个线程。可以是其他线程的ID也可以传自己的线程IDpthread_self()返回的就是自己的ID。默认情况下新创建的线程是joinable的线程退出后必须由其他线程调用pthread_join来回收资源否则线程占用的栈空间和内核描述符不会被释放造成内存泄漏。线程泄漏和进程泄漏不一样进程泄漏是变成僵尸进程占着PID线程泄漏是占着栈空间和描述符不会变僵尸但会慢慢耗尽系统资源能创建的线程总数有限泄漏多了后面就创建不了新线程了。如果不关心线程的返回值pthread_join就是一种负担主线程要卡在那里等可能影响程序整体的响应速度。这个时候可以把线程设置为分离状态detach告诉系统这个线程退出时自动释放资源不需要别人等它。这就是线程分离的本质。joinable和detach是冲突的一个线程不能既是joinable又是detach。默认创建出来的是joinable状态调用了pthread_detach就变成detach状态变不回去。所以新创建的线程默认是不分离的必须显式调用pthread_detach才能变成分离状态。就算线程被分离了它也还是和其他线程共享同一块地址空间照样能访问进程里的所有全局变量、堆内存、文件描述符。分离只是针对资源回收方式的设置不影响线程访问共享资源的能力。一个分离的线程如果访问了已经被释放的内存或者修改了全局变量照样会影响其他线程。#include iostream #include pthread.h #include unistd.h using namespace std; void* thread_func(void* args) { pthread_detach(pthread_self()); for (int i 0; i 3; i) { cout 子线程: i endl; sleep(1); } return nullptr; } int main() { pthread_t tid; pthread_create(tid, nullptr, thread_func, nullptr); for (int i 0; i 5; i) { cout 主线程: i endl; sleep(1); } return 0; }子线程函数里先调用pthread_detach(pthread_self())把自己分离然后循环打印3次每次睡1秒。主线程创建子线程后直接进入循环打印5次每次睡1秒不等子线程。主线程循环结束后直接return 0程序退出。因为子线程已经分离了所以主线程不用管子线程子线程退出的时候系统会自动回收资源。二. 线程ID及进程地址空间布局1.线程ID和进程ID在Linux中目前的线程实现是Native POSIX Thread Library简称NPTL。在这种实现下线程又被称为轻量级进程每一个用户态的线程在内核中都对应一个调度实体拥有自己的进程描述符task_struct结构体。多线程的进程又被称为线程组线程组内的每一个线程在内核中都有一个task_struct与之对应。task_struct中的pid字段对应的是线程ID不是进程ID。task_struct中的tgid字段是Thread Group ID对应的是用户层面看到的进程ID。用户态概念系统调用内核task_struct字段线程IDsyscall(SYS_gettid)pid进程IDgetpid()tgid这里说的线程ID和pthread_t类型的线程ID不一样。pthread_t是pthread库自己维护的线程标识而gettid返回的是内核层面的线程ID是pid_t类型用来在内核中唯一标识一个线程。ps命令加上-L选项会显示LWP和NLWP两列。LWP是Light Weight Process就是线程ID也就是gettid系统调用的返回值。NLWP是线程组内线程的个数。举例说明一个进程里有三个线程主线程pid28543tgid28543线程Bpid28544tgid28543线程Cpid28545tgid28543它们的tgid都是28543这就是用户态看到的进程ID。而pid各不相同是内核用来区分各个线程的ID。Linux提供了gettid系统调用用来返回线程ID但glibc没有封装这个系统调用不能直接调用。如果确实需要获得内核线程ID可以用syscall函数#include sys/syscall.h pid_t tid syscall(SYS_gettid);线程组内的第一个线程在用户态被称为主线程在内核中被称为group leader。内核在创建第一个线程时会将tgid的值设置成第一个线程的pidgroup_leader指针指向自身。所以线程组内主线程的pid等于tgid也就是进程ID。内核源码里是这样设置的p-tgid p-pid; p-group_leader p;对于线程组里的其他线程内核分配新的pid但tgid设置为当前线程的tgid也就是主线程的pid。group_leader指向主线程的task_struct。if (clone_flags CLONE_THREAD) { p-tgid current-tgid; p-group_leader current-group_leader; }线程和进程不一样进程有父进程的概念父进程和子进程是层级关系。但线程组里面所有的线程都是对等关系没有父子之分只有主线程和普通线程的区别它们共享同一个地址空间和资源。补充pthread_t和内核线程ID的区别pthread_t是pthread库在用户态维护的线程标识符pthread_self()返回的就是这个值它是一个不透明的类型可能是个结构体指针也可能是个整数不同平台实现不一样不能直接当作整数用。gettid返回的是内核层面的线程ID是pid_t类型是个整数内核用它来调度和管理线程。两者没有直接关系一个是用户态库的标识一个是内核的标识。2.线程ID及进程地址空间布局和对原生线程库的理解pthread_ create函数会产⽣⼀个线程ID存放在第⼀个参数指向的地址中。该线程ID和前⾯说的线程ID不是⼀回事。前⾯讲的线程ID属于进程调度的范畴。因为线程是轻量级进程是操作系统调度器的最⼩单位所以需要⼀个数值来唯⼀表⽰该线程。pthread_ create函数第⼀个参数指向⼀个虚拟内存单元该内存单元的地址即为新创建线程的线程ID属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数可以获得线程⾃⾝的IDpthread_t pthread_self(void);pthread_t 到底是什么类型呢取决于实现。对于Linux⽬前实现的NPTL实现⽽⾔pthread_t类型的线程ID本质就是⼀个进程地址空间上的⼀个地址。1.Linux线程的本质Linux操作系统没有真正意义上的线程而是用进程来模拟的所以叫轻量级进程LWP。系统本身没有提供线程创建、终止、等待、分离这些系统调用接口但是提供了clone系统调用来创建轻量级进程。为了更好用系统在用户态封装了一个原生线程库pthread这样用户就能用pthread_create这些函数来操作线程了。系统通过PCB来调度管理线程用户层通过pthread库来管理线程的属性数据。2.线程相关的ID有两个。一个是LWP属于进程调度范畴。线程是轻量级进程是操作系统调度的最小单位内核需要一个数值来唯一标识它这个数值就是LWP。ps命令加上-L选项看到的LWP列就是这个值gettid系统调用返回的也是这个值。另一个是pthread_t类型的线程ID是pthread_create的第一个参数返回的。这个ID属于用户层范畴是pthread库自己维护的标识库的后续操作都靠这个ID来找到对应的线程。这个ID本质是一个地址指向共享区中pthread库维护的某个结构体的起始位置。两个ID是一一对应的关系每个用户态线程在内核里都有一个LWP。3.pthread库在内存中的布局原生线程库libpthread.so在磁盘上是一个动态库文件程序运行时被加载到内存映射到共享区。这样所有线程都能看到这个库的代码和数据。pthread库在共享区中为每个线程维护了一个管理块类型是struct pthread里面存放线程的所有属性数据包括线程ID、状态、优先级、上下文、栈地址、线程局部存储等。这个结构体就是用户态线程的TCB。pthread_t类型的线程ID本质上就是struct pthread结构体在共享区中的起始地址。拿到这个地址就能在库中找到线程的所有属性数据。4.线程栈每个线程都必须有自己独立的栈空间用来保存线程运行时的中间数据、局部变量、函数调用链。主线程的栈用的是地址空间的栈区叫主线程栈不经过pthread库分配。子线程的栈是pthread库在共享区或者堆上分配的由库管理。图片里画了共享区中多个struct pthread结构体和对应的线程栈每个线程的TCB和栈都在共享区里相邻排列。5.pthread_create作用pthread_create底层调用了clone系统调用。clone的参数包括入口函数地址、栈地址、标志位等。传入CLONE_THREAD标志让新进程和父进程共享地址空间、文件描述符、信号处理器等这样看起来就像线程了。内核为这个新创建的轻量级进程分配task_struct结构体分配LWP加入调度队列。pthread库在共享区为这个线程分配struct pthread结构体和线程栈初始化属性数据。最后把struct pthread的地址作为pthread_t类型的线程ID返回给用户。6.内核和用户态的联动内核里的task_struct对于每个线程都有一个里面记录pid、tgid、状态、优先级等。用户层的pthread库结构体和内核的task_struct通过LWP关联起来但两者是独立的一个在用户态管理一个在内核态管理。用户态代码不知道LWP的值内核不知道pthread_t的值它们通过系统调用和1:1的映射关系联动。用户态和内核态通过系统调用传递数据比如pthread_join调用时用户态传入pthread_t库根据这个地址找到对应的struct pthread从里面拿到LWP然后通过系统调用让内核等待这个LWP结束获取退出状态写入struct pthread再返回给用户。7.用户线程和LWP的映射关系Linux采用的是1:1模型每个用户态线程对应内核中的一个LWP。其他系统有一对多或者多对多的模型但Linux用1:1调度简单每个线程都是独立的调度实体。实际上很多操作系统在设计线程时都是用户级线程和内核级线程配合使用的。用户级线程把相关的属性数据放在用户层管理真正的调度还是得由内核级线程来处理。Linux采用的是1:1模型一个用户线程对应一个内核线程。也有1:N的模型用户层只有一个执行流但内核层有多个执行流去跑这个任务。还有M:N模型用户层M个线程对应内核层N个执行流。Linux选1:1是为了调度简单每个线程都是独立的调度实体互不干扰。用户级线程和内核级线程怎么关联其实就是用户级线程把要执行的代码交给内核级线程去跑。创建用户线程就是创建一个LWP退出用户线程就是退出对应的LWP再清理库里的数据。用户层的操作能和内核层对应上就行了。打个比方这就像公司里项目经理和一线员工的关系。项目经理接到客户需求把任务拆解分配给下面的员工去干员工负责具体执行项目经理负责对接客户、汇报进度。客户不需要知道具体是哪个员工在干活只需要和项目经理沟通就行。项目经理就是用户级线程一线员工就是内核级线程一一对应。客户就是应用程序只需要调pthread_create、pthread_join这些接口就行不需要知道底层LWP怎么创建的、栈怎么分配的。Linux需要原生线程库的原因就是Linux本身没提供真正的线程只提供了轻量级进程和clone接口。用户直接操作LWP的话要自己分配栈、设置clone标志、管理资源太麻烦了。所以封装一个pthread库提供简单接口用户只管创建线程、等待线程、分离线程底层的脏活累活库和内核去干。就像客户只管提需求项目经理去安排谁干活、怎么干客户不用关心具体执行细节。三.线程封装这个Thread.hpp封装了一个线程类把pthread的C风格接口包装成C的类让用户用起来更方便。类里包了这些东西线程ID、线程名字、是否分离、是否在运行、退出码、线程要执行的函数。用户创建一个Thread对象传进去一个任务函数调用Start就开始跑了Stop停止Join等待结束Detach分离。代码如下Main.cc#include Thread.hpp #include unistd.h using namespace ThreadModlue; int main() { Thread t([]{ while(true) { std::cout 我是一个新线程 std::endl; sleep(1); } }); t.Start(); t.Detach(); sleep(5); t.Stop(); sleep(5); t.Join(); return 0; }这段代码创建了一个线程对象t传进去一个lambda作为线程要执行的函数。然后调用Start启动线程线程开始跑while循环每秒打印一次。接着Detach把线程分离分离后主线程不再管它。主线程睡5秒子线程在这5秒内一直在打印。然后Stop取消线程线程停止。再睡5秒最后Join等待线程结束。但因为线程已经被分离了Join会打印线程已经是分离状态不能join然后返回。Thread.hpp这个文件定义了一个Thread类把pthread的C风格接口封装成C类。jiexiruxai1#ifndef _THREAD_H_ #define _THREAD_H_ #include iostream #include string #include pthread.h #include cstdio #include cstring #include functional namespace ThreadModlue { static uint32_t number 1; class Thread { using func_t std::functionvoid(); private: void EnableDetach() { std::cout 线程被分离了 std::endl; _isdetach true; } void EnableRunning() { _isrunning true; } static void* Routine(void* args) { Thread* self static_castThread*(args); self-EnableRunning(); if (self-_isdetach) self-Detach(); pthread_setname_np(self-_tid, self-_name.c_str()); self-_func(); return nullptr; } public: Thread(func_t func) : _tid(0), _isdetach(false), _isrunning(false), _res(nullptr), _func(func) { _name thread- std::to_string(number); } void Detach() { if (_isdetach) return; if (_isrunning) pthread_detach(_tid); EnableDetach(); } bool Start() { if (_isrunning) return false; int n pthread_create(_tid, nullptr, Routine, this); if (n ! 0) { std::cerr create thread error: strerror(n) std::endl; return false; } std::cout _name create success std::endl; return true; } bool Stop() { if (!_isrunning) return false; int n pthread_cancel(_tid); if (n ! 0) { std::cerr cancel thread error: strerror(n) std::endl; return false; } _isrunning false; std::cout _name stop std::endl; return true; } void Join() { if (_isdetach) { std::cout 线程已经是分离状态不能join std::endl; return; } int n pthread_join(_tid, _res); if (n ! 0) { std::cerr join thread error: strerror(n) std::endl; } else { std::cout join success std::endl; } } ~Thread() {} private: pthread_t _tid; std::string _name; bool _isdetach; bool _isrunning; void* _res; func_t _func; }; } #endif解析如下namespacenamespace ThreadModlue { }把Thread类放在ThreadModlue命名空间里防止和其他代码的名字冲突。拼写错了应该是ThreadModule。静态成员变量static uint32_t number 1;用来给线程起名字每创建一个线程就加1所以线程名字是thread-1、thread-2、thread-3这样排下去。这是个静态成员变量所有Thread对象共享同一个number。类型别名using func_t std::functionvoid();定义了任务函数的类型是一个返回值void、没有参数的函数。用std::function包起来可以接收lambda、函数指针、函数对象、bind表达式等等。私有成员变量pthread_t _tid; // 线程IDpthread库用的 std::string _name; // 线程名字自己起的 bool _isdetach; // 是否已经被分离 bool _isrunning; // 线程是否在运行 void* _res; // 线程退出码 func_t _func; // 线程要执行的任务函数私有成员函数EnableDetachvoid EnableDetach() { std::cout 线程被分离了 std::endl; _isdetach true; }标记线程为分离状态打印提示。EnableRunningvoid EnableRunning() { _isrunning true; }标记线程为运行状态。Routinestatic void* Routine(void* args) { Thread* self static_castThread*(args); self-EnableRunning(); if (self-_isdetach) self-Detach(); pthread_setname_np(self-_tid, self-_name.c_str()); self-_func(); return nullptr; }这是pthread_create要求的入口函数。必须是静态的因为普通成员函数有隐藏的this指针跟pthread要求的函数签名void()(void*)对不上。传进来的参数是Thread对象的指针Start里面传的是this。Routine先转回Thread*然后标记线程正在运行检查如果已经分离了就调用Detach设置线程名字最后调用用户的任务函数_func()。任务函数执行完线程就结束了。公有成员函数构造函数Thread(func_t func) : _tid(0), _isdetach(false), _isrunning(false), _res(nullptr), _func(func) { _name thread- std::to_string(number); }接收一个任务函数初始化所有成员变量线程名字自动生成。_isdetach和_isrunning初始都是false。Detachvoid Detach() { if (_isdetach) return; if (_isrunning) pthread_detach(_tid); EnableDetach(); }分离线程。如果线程已经在运行了调用pthread_detach真正分离。如果线程还没运行先标记_isdetach为true等Routine里看到_isdetach是true的时候再调用Detach。已经分离过了就直接返回。Startbool Start() { if (_isrunning) return false; int n pthread_create(_tid, nullptr, Routine, this); if (n ! 0) { std::cerr create thread error: strerror(n) std::endl; return false; } std::cout _name create success std::endl; return true; }启动线程。如果已经在运行就返回false。调用pthread_create创建线程第四个参数传thisRoutine拿到this就能操作当前对象。创建成功打印成功信息失败打印错误信息。Stopbool Stop() { if (!_isrunning) return false; int n pthread_cancel(_tid); if (n ! 0) { std::cerr cancel thread error: strerror(n) std::endl; return false; } _isrunning false; std::cout _name stop std::endl; return true; }停止线程。底层调pthread_cancel取消线程成功标记_isrunning为false。线程不在运行就直接返回false。Joinvoid Join() { if (_isdetach) { std::cout 线程已经是分离状态不能join std::endl; return; } int n pthread_join(_tid, _res); if (n ! 0) { std::cerr join thread error: strerror(n) std::endl; } else { std::cout join success std::endl; } }等待线程结束。如果线程已经分离了不能join打印提示返回。否则调用pthread_join阻塞等待线程结束的时候退出码存到_res里。成功打印join success失败打印错误信息。析构函数~Thread() {}整体思路创建Thread对象传任务函数。调用Startpthread_create创建线程入口是Routine。Routine拿到this指针标记运行状态检查分离标记调用用户的任务函数。用户任务函数执行完线程结束。主线程可以调用Join等待结束拿退出码或者调用Detach让线程自己回收。也可以调用Stop取消线程。Makefile运行结果四.知识补充虽然Linux将进程和线程统一到了task_struct结构体进行管理但对待地址空间中的栈还是有些区别的。进程栈对于Linux进程或者说主线程来说栈就是main函数的栈空间。在fork的时候子进程复制了父进程的栈空间地址通过写时拷贝技术实现共享栈空间可以动态增长。如果栈的增长超出了ulimit设置的上限就会触发段错误操作系统发送段错误信号给进程。进程栈比较特殊它是唯一一种可以访问未映射页面而不一定会触发段错误的情况只有超出栈的扩充上限才会报段错误。线程栈对于主线程生成的子线程来说栈不是动态增长的而是事先固定下来的。线程栈一般是通过调用pthread库接口pthread_create创建线程时分配的位置在文件映射区也就是共享区。底层使用mmap系统调用分配这在glibc的nptl/allocatestack.c中的allocate_stack函数里可以看到mem mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);这个调用中的size参数获取比较复杂用户可以手动传入栈的大小也可以使用默认值一般是8MB。关键点在于线程栈不能动态增长一旦用尽就没了这和fork出来的进程栈不一样。在glibc中通过mmap得到栈空间之后底层会调用sys_clone系统调用int sys_clone(struct pt_regs *regs) { unsigned long clone_flags; unsigned long newsp; int __user *parent_tidptr, *child_tidptr; clone_flags regs-bx; newsp regs-cx; // 获取mmap得到的线程栈指针 parent_tidptr (int __user *)regs-dx; child_tidptr (int __user *)regs-di; if (!newsp) newsp regs-sp; return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr); }注意子线程的栈本质上是进程地址空间中mmap出来的一块内存区域原则上是线程私有的。但是同一个进程的所有线程在生成的时候会浅拷贝创建者的task_struct的很多字段所以如果愿意其他线程也可以访问这块内存。这就意味着线程栈虽然设计上是私有的但实际上在同一个进程中并不是绝对安全的其他线程如果拿到了栈地址照样可以读写编程时一定要注意这个问题。线程局部存储 __thread多线程编程里全局变量是所有线程共享的。一个线程改了其他线程都能看到。有时候我们确实需要这样比如多个线程一起统计总数。但有时候我们想要的是每个线程都有自己的独立变量互不干扰。比如每个线程都要记录自己的错误码自己的缓存数据自己的随机数种子。如果用全局变量线程之间会互相覆盖。如果用局部变量函数之间传递又麻烦。线程局部存储就是解决这个问题的。1.__thread__thread是 GCC 的一个关键字用来声明线程局部变量。被它修饰的变量每个线程都有一份独立的副本。一个线程改了自己的其他线程不受影响。__thread int count 1;这行定义了一个全局变量但每个线程都有自己的count各是各的。3.代码如下#include pthread.h #include iostream #include string #include unistd.h __thread int count 1; void* thread1(void* args) { while(true) { std::cout thread1: count count std::endl; count; sleep(1); } } void* thread2(void* args) { while(true) { std::cout thread2: count count std::endl; sleep(1); } } int main() { pthread_t t1, t2; pthread_create(t1, nullptr, thread1, nullptr); pthread_create(t2, nullptr, thread2, nullptr); pthread_join(t1, nullptr); pthread_join(t2, nullptr); return 0; }4.运行结果thread1 的count一直往上加thread2 的count始终是1。两个线程完全不影响。打印两个线程里count的地址可能看到相同的数字。但那是虚拟地址不同线程的虚拟地址映射到不同的物理内存。所以地址看着一样实际存的地方不同数据自然也不同。5.普通全局变量和 __thread 的区别普通全局变量所有线程共享一个一个改了全部变。__thread变量每个线程各有一份各改各的。普通全局变量访问需要加锁__thread不需要因为压根不共享。6.使用限制__thread只能修饰简单的类型比如int、char、指针这些。不能修饰复杂的C对象比如string、vector。如果需要复杂对象可以用C11的thread_local。thread_local std::string name hello;7.底层原理__thread变量存在pthread库管理的线程局部存储区里每个线程创建的时候分配一份线程退出的时候释放。编译器通过当前线程的控制块找到对应的存储位置所以同一个变量名在不同线程里访问的是不同的内存。__thread不是C标准是GCC自己的扩展换编译器可能不支持。C11的thread_local是标准的推荐用那个。__thread不能修饰局部变量只能修饰全局变量或者静态变量。线程局部存储就是让全局变量变成线程私有的。多线程编程里该共享的用普通全局变量加锁该私有的用__thread各干各的清晰又高效。