线程理解和控制

📅 2026/7/13 21:21:37
线程理解和控制
理解线程1.初步理解定义进程内核数据结构 pcb 代码数据执行流是分配系统资源的实体线程进程内部独立执行分支是CPU 调度基本单位共享进程的绝大部分资源结论1Linux 不区分进程 / 线程内核结构统一用task_struct模拟线程称为轻量级进程 (LWP)widowsTCB描述线程单进程一个task_struct绑定独立mm_struct多线程多个task_struct所有线程共享进程地址空间、堆、全局内存、文件句柄、信号、标准 IO没有隔离。2资源隔离 / 共享本质是mm_struct虚拟地址空间的归属划分比如把.text代码段的虚拟地址划分后分给许多个线程分别执行里面的不同函数即可函数就是一部分虚拟地址的集合--后面的pthread_create的线程执行函数就是让线程去执行这个函数划分得到相应的虚拟地址空间struct page页框和页页框对应虚拟地址页框对应物理地址全系统统一 4KB 块磁盘物理内存切分为 4KB 大小页帧页框虚拟地址空间也按 4KB 分页OS要管理这么多的4KB页框先描述再组织先描述通过struct pagestruct page { unsigned long flags; // 页状态标记是否脏页、是否锁定、是否可交换等 union { struct list_head lru; // LRU链表节点用于页面回收active/inactive链表 void *mapping; // 指向文件address_space要是匿名页为NULL } } .........再组织现在可以理解为通过全局数组mem[]管理所有物理页4GB 物理内存可划分页框总数为4GB \ 4KB 1048576个)下标等价物理页框编号知道了下标index*4就知道了该页框的起始物理地址再加上偏移量就能找到一个页框的每一kb的具体地址了所以申请物理内存1.查数组改page2.建立内核数据结构映射虚拟地址、进程、物理页三者关联。radix_tree基数树strcut file-struct address_space-struct radix_tree_root-struct radix_tree_nodestruct radix_tree_node { unsigned int count; void *slots[RADIX_TREE_MAP_SIZE]; // 子节点/struct page指针槽位 unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS]; // 页标签脏页、写回标记 };slots[]每一个元素存储的就是struct page *指针即通过radix_tree可以快速找到对应structpage页表MMU 地址转换完整流程32 位虚拟地址分为三段高 10 位 (页目录) 中间 10 位 (页表) 低 12 位 (页内偏移)10位对应2的10次方102412位4096刚好和页目录表1024个页表1024个物理内存对应从 CR3 取出页目录物理地址用虚拟地址高 10 位作为下标查页目录得到目标页表的物理地址以该页表物理地址为基用虚拟地址中间 10 位作为下标查页表得到目标物理页框的起始地址将物理页框基地址 虚拟地址低 12 位偏移拼接出完整物理地址访问内存对应字节。CR3 → 页目录(高10位索引) → 页表(中间10位索引) → 物理页框基地址 低12位偏移 物理地址单级页表对连续内存要求高于是引入了多级页表但是多级页表也是⼀把双刃剑在减少连续存储要求且减少存储空间的同时降低了查询效率。MMU 引入 TLB高速缓存虚拟地址到物理地址映射CPU 把虚拟地址交给 MMU 做地址转换分两条分支TLB 命中快路 First直接从 TLB 高速缓存拿虚实页映射快速生成物理地址访存耗时短。TLB 缺失慢路 Second去内存读取页表获取映射多一次内存开销查到后把新映射刷新存入 TLB方便下次加速。线程的“私有”资源1线程ID2⼀组寄存器3线程的上下文数据4栈 errno5信号屏蔽字6调度优先级进程切换→ 保存硬件上下文cpu寄存器集合存入 PCB → 保存进程独有资源上下文页表、文件描述符、内存映射 → 刷新 TLB、清空缓存映射 → 切换 CR3 寄存器 → 读取新进程 PCB分别恢复它的资源信息和硬件上下文 → 新进程开始运行线程切换仅保存 / 恢复线程私有通用寄存器、栈指针等等不切换页表、不刷新 TLB、不更换地址空间共享页表与缓存更轻量化的主要原因线程优点1.开销更低2.计算密集型应用多核同时干活缩短计算耗时3.I/O密集型应用IO 等待不闲着填满 CPU 空闲时间线程缺点性能损耗计算密集线程过多时会产生大量同步、调度开销反而拖慢程序。尽量线程和CPU数量一样不会来回调度程序健壮性差线程共享进程资源无隔离保护共享变量极易因调度时序出现竞争异常开发调试难度大编写与调试⼀个多线程程序比单线程程序困难得多线程访问控制粒度粗线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进而该进程内的所有线程也就随即退出Linux线程控制pthread_create创建线程#include pthread.h int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);参数解析pthread_t *thread输出型参数成功后写入新线程 IDconst pthread_attr_t *attr线程属性填NULL使用默认属性void *(*start_routine)(void *)线程执行函数函数指针入参 / 返回值都是void*void *arg传给线程函数的参数返回值规则成功返回0失败返回错误码不修改全局 errno*thread内容无意义小知识perror strerror特性perrorstrerror错误来源自动读全局 errno手动传入 int 错误码适用不会自动设置errno的输出行为直接打印到 stdout返回 char * 字符串不打印egint ret pthread_create(...);if(ret ! 0) printf(err: %s, strerror(ret));pthread_join 等待回收线程不回收线程资源会产生类僵尸线程造成内存泄漏#include pthread.h int pthread_join(pthread_t thread, void **retval);参数解析pthread_t thread要等待的目标线程 IDvoid **retval输出参数接收线程函数的返回值不需要返回值填NULL不提供线程崩溃的异常字段只负责回收正常跑完的线程线程崩溃会直接干掉整个进程join 来不及处理所以函数参数里没有任何异常 / 崩溃相关字段。pthread_self 获取当前线程 ID#include pthread.h pthread_t pthread_self(void);哪一个线程调用就返回哪一个线程的id这些接口其实是经过封装的pthread库Linux 内核根本无原生线程只有 LWP 轻量级进程线程是用户层概念内核统一把所有执行流视作进程线程本质是共享父进程资源的 LWP。pthread 库作用封装底层复杂的clone系统调用对外提供简单标准线程 API屏蔽 LWP开发者只操作pthread_t线程 ID。pthread_create 底层就是调用 clone所以编译必须加-pthread用于链接 pthread 库。demo代码#includeiostream #includestring #includepthread.h #includecstdio #includeunistd.h using namespace std; // 打印线程pthread_t句柄 void showtid(const pthread_t tid) { printf(main的showtid:0x%lx\n,tid); } // 把线程ID转为十六进制字符串 string Formaid(const pthread_t tid) { char id[64]; snprintf(id,sizeof(id),0x%lx,tid); return id; } // 子线程执行函数 void *routine(void* args) { // 把传入的参数转为字符串 string namestatic_castconst char*(args); // 获取当前线程自身ID pthread_t tidpthread_self(); int cnt5; while(cnt) { cout我是一个新线程,我的名字是:name我的Formatid是Formaid(tid)endl; sleep(1); cnt--; } return (void*)123; // 线程返回值 } int main() { pthread_t tid; // 创建子线程传参 thread-1 pthread_create(tid,nullptr,routine,(void*)thread-1); showtid(tid); int cnt5; while(cnt) { // 主线程循环打印 cout我是main线程,我的名字是:thread-main我的Formatid是 Formaid(pthread_self())endl; sleep(1); cnt--; } void* retnullptr; // 阻塞等待子线程结束获取返回值 pthread_join(tid,ret); coutret:(long long int)retendl; return 0; }运行结果打印的顺序每次不一样 CPU 调度时随机挑选就绪线程执行创建线程、sleep 唤醒就绪后这两个过程在调度时选择都不固定每次运行打印先后不一样ps -aL:1LWPLight Weight Process轻量级进程 ID 内核真实 TIDCPU 调度时识别线程的唯一标识2CPU 调度时看的是LWP,不是 PID同一进程多个 LWP的时间片会被调度器平等分时执行。线程的结束方式给线程传递的参数和返回值可以是任意类型(包括对象)eg核心逻辑创建一个子线程在线程内部计算两个数字的和把计算结果封装成对象传回主线程主线程打印最终结果。#include iostream #include pthread.h using namespace std; // 任务类保存待计算的两个数字提供加法计算接口 class Task { public: // 构造函数初始化两个运算数 Task(int a, int b) : _a(a), _b(b) { } // 执行计算返回两个数字相加的结果 int Execute() { return _a _b; } // 析构函数 ~Task() {} private: int _a; // 加数1 int _b; // 加数2 }; // 结果类封装线程计算得到的结果值 class Result { public: // 构造函数存入计算结果 Result(int res) : _res(res) { } // 获取保存的计算结果 int GetResult() { return _res; } ~Result() {} private: int _res; // 存储加法结果 }; // 子线程执行函数pthread规定固定签名 void*(void*) void *routine(void *args) { // 把传入的void*通用参数强转为Task任务指针 Task *t (Task *)args; // 错误写法创建栈局部Result对象函数结束对象销毁返回res会生成野指针 // Result res(t-Execute()); // return res; // 正确写法new在堆上创建Result堆内存不会随线程函数销毁 Result* res new Result(t-Execute()); // 返回堆对象地址主线程可以安全访问 return res; } int main() { pthread_t tid; // 存储子线程ID // 在主线程栈上创建任务对象传入计算数字10、20 Task t(10, 20); // 创建子线程传入任务对象地址给子线程 pthread_create(tid, nullptr, routine, t); Result* ret nullptr; // 阻塞等待子线程运行结束接收子线程返回的堆Result对象指针 pthread_join(tid, (void**)ret); // 从Result对象中取出最终计算值 int n ret-GetResult(); cout 最后计算结果是 n endl; // 释放堆上创建的Result避免内存泄漏 delete ret; ret nullptr; return 0; }运行结果1.return返回方式return执行后线程自然结束了2.exit什么结果都没有因为exit是终止进程的调用exit后整个线程都没有了自然没有结果了所以线程退出不能用exit3.pthread_exit()void pthread_exit(void *retval);void *routine(void *args) { Task *t (Task *)args; Result* res new Result(t-Execute()); // 等价于 return res; pthread_exit(res); // 下面代码永远不会执行 cout 这段代码不会跑 endl; }运行结果主线程用它不会杀死子线程普通子线程用它等价于函数 return。4.pthread_cancelint pthread_cancel(pthread_t thread);返回值0 成功非 0 失败1pthread_cancel(pthread_self())会终止主线程但子线程继续运行和pthread_exit效果类似。2线程被取消时join拿到的返回值固定为 PTHREAD_CANCELED3线程只会在取消点真正退出sleep / read / write / pthread_join / pthread_mutex_lock等。没有的话调用了也无效eg运用void *routine(void *args) { Task *t (Task *)args; // 循环计算sleep是取消点 while(1) { cout 子线程运行中计算 t-Execute() endl; sleep(1); // 取消点收到cancel会在这里退出 } return nullptr; } int main() { pthread_t tid; Task t(10, 20); pthread_create(tid, nullptr, routine, t); sleep(3); // 让子线程跑3秒 cout 主线程发起取消子线程请求 endl; pthread_cancel(tid); // 发送取消信号 void* ret nullptr; pthread_join(tid, ret); // 线程被取消时join拿到的返回值固定为 PTHREAD_CANCELED if(ret PTHREAD_CANCELED) { cout 子线程是被取消终止的 endl; } return 0; }运行结果默认情况下新创建的线程是joinable的线程退出后需要对其进行pthread_join操作否则无法释放资源从而造成系统泄漏。如果不关心线程的返回值join是⼀种负担这个时候我们可以告诉系统当线程退出时自动释放线程资源。类似进程signal(SIGCHLD, SIG_IGN);int pthread_detach(pthread_t thread);线程组内其他线程对目标线程进行分离也可以是线程自己分离分离的线程依旧在进程的地址空间中进程的所有资源被分离的线程依旧可以访问可以操作。void *routine(void *args) { string name (const char *)args; pthread_detach(pthread_self());//也可以写到新线程里面 cout 我是new线程名字 name endl; return nullptr; } int main() { pthread_t tid; pthread_create(tid, nullptr, routine, (void *)thread-1); // 主线程 pthread_detach(tid);//写到主线程 cout 主线程分离新线程 endl; // 判断分离过后还能不能等待 int n pthread_join(tid, nullptr); if (n ! 0) { cout pthread_join error endl; } else { cout pthread_join success endl; } return 0; }joinable和分离是冲突的⼀个线程不能既是joinable又是分离的。运行结果线程ID及进程地址空间布局mmap区域pthread 创建子线程时用 mmap 分别分配子线程独立栈空间、分配TCB 管理内存pthread 库 TCB 线程管理块struct pthread TCB线程控制块存在进程 mmap 分配的库内存里pthread_t tid本质就是这个结构体的虚拟地址每个 TCB 包含三块私有资源struct pthread管理元数据状态、返回值、属性等线程局部存储 TLS独立线程栈mmap 申请的线程的私有栈线程返回值 pthread_join 原理线程函数return/pthread_exit()传入的指针 / 数值会保存在自身TCB 结构体内部线程退出后 TCB 不会销毁留存返回值。pthread_join(tid, ret);所以传入的tid就是TCB的的虚拟地址这样就能找到这个结构体里面存储的返回值了pthread_join流程主线程调用pthread_join(tid, ret)阻塞等到目标子线程执行完毕从子线程的 TCB 里取出预先存放的void*返回值拷贝到主线程局部变量ret销毁释放子线程 TCB、线程栈、线程局部存储完成资源回收Linux 用户级线程 : 内核LWP 1:1。每调用一次pthread_create创建一个用户线程内核就配套生成一个独立轻量级进程 LWPCPU 调度直接操作 LWP。pthread_create的调用逻辑3件事int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);分配线程栈,TCB线程局部存储底层封装 clone 系统调用内核创建 LWPcpu调度线程栈主线程进程 main 栈特性1来源fork 复制父进程栈2可动态自动扩容3缺页容错特殊允许访问未映射页、不一定直接段错误的栈线程栈mem mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);标志MAP_STACK专门标记这块内存用作线程栈默认固定 8MB 大小一般够用不支持动态扩容空间用完直接栈溢出崩溃属于进程虚拟地址里一块独立匿名映射区域pthread_create → allocate_stackmmap 分配栈→ 调用sys_cloneclone封装了sys_clone)系统调用 clone 时把 mmap 得到的栈顶栈向下增长newsp传给内核内核新建 task_struct (LWP) 绑定该栈。int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);线程之间虽说是“私有”的线程栈但线程之间也可以访问子线程的a变量虽然定义在其“私有”的线程栈上但因为线程之间的虚拟地址空间是共享的所以其它线程当然也可以通过地址访问到a变量。即只要拿到栈内变量地址线程语法上能访问但不推荐跨线程读写对方栈子线程退出时 mmap 栈会被 munmap 释放指针变成野指针属于 C/C 未定义行为不同系统 / 内核表现不一致有时正常、有时段错误。C封装线程先再认识2个接口设置线程名// 参数1 线程tid参数2 字符串名字 int pthread_setname_np(pthread_t thread, const char *name);获取线程名char buf[16]; pthread_getname_np(pthread_t tid, buf, sizeof(buf));pthread.hpp:#pragma once #include iostream #include pthread.h #include unistd.h #include functional #include string #include errno.h #include cstring #includecstdint using namespace std; namespace ThreadModlue { static uint32_t number 1;//bug多个线程同时构造时number存在数据竞争会出现重名线程 //定义在 cstdint不用int是两点1. 线程编号只能是正数 //2. uint32_t固定 32 位跨平台不会溢出 using func_t functionvoid(); class Thread { private: void EnableRunning() { cout_isrunningtrueendl; _isrunning true; } void EnableDetach() { cout _isdetachtrue endl; _isdetach true; } static void *Routine(void *args) { Thread *self static_castThread *(args); self-EnableRunning(); self-Detach(); pthread_setname_np(self-_tid, self-_name.c_str()); self-_func(); // 回调处理 return nullptr; } //C 普通成员函数隐藏第一个参数 this函数真实原型,所以Routine真实原型 //void Routine(Thread* this, void* args);与pthread_create 对线程入口函数的强制要求 //void* (*start_routine)(void*);只接收单个 void * 参数不能多参数。有冲突所以加static //static成员函数没有隐含this指针 public: Thread(func_t func) : _isdetach(false), _isrunning(false), _tid(0), _func(func) { _name thread- std::to_string(number); } void Detach() { if (_isrunning _isdetach) { cout调用了phread_detach,线程分离了endl; pthread_detach(_tid); } else if (_isdetach false) 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; } else { std::cout _name create success std::endl; return true; } } bool Stop() { if (_isrunning) { int n pthread_cancel(_tid); if (n ! 0) { std::cerr stop thread error: strerror(n) std::endl; return false; } else { _isrunning false; std::cout _name stop std::endl; return true; } } return false; } bool Join() { if (_isdetach) { return false; } int n pthread_join(_tid, nullptr); if (n ! 0) { std::cerr join thread error: strerror(n) std::endl; return false; } else { std::cout _name join std::endl; return true; } } ~Thread() { } private: pthread_t _tid; bool _isrunning; bool _isdetach; func_t _func; string _name; }; };main.cc:#include pthread.hpp #include unistd.h #include vector using namespace ThreadModlue; void func() { while (1) { char name[128]; pthread_getname_np(pthread_self(), name, sizeof(name)); std::cout 我是一个新线程: name std::endl; // 我的线程的名字是什么呀debug sleep(1); } } int main() { Thread t(func); t.Detach(); t.Start(); sleep(5); t.Stop(); sleep(5); t.Join(); return 0; }运行结果线程局部存储TLS#include iostream #include pthread.h #include unistd.h #include cstring #include stdio.h using namespace std; static int count 0; string Addr(int c) { char addr[64]; snprintf(addr, sizeof(addr), %p, c); return addr; } void *routine1(void *args) { (void)args; while (1) { cout thread-1,count count [我来修改count], count: Addr(count) endl; count; sleep(1); } } void *routine2(void *args) { (void)args; while (1) { cout thread-2,count count count: Addr(count) endl; count; sleep(1); } } int main() { pthread_t tid1, tid2; pthread_create(tid1, nullptr, routine1, nullptr); pthread_create(tid2, nullptr, routine2, nullptr); pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); return 0; }运行结果我们可以从运行结果看出count的地址并没有发生变化再换成__thread int count0后发现两个线程的count不一样了互不影响线程局部存储原理操作系统为每个线程单独分配 TLS 内存段线程访问时自动切换自身副本地址各不相同互不干扰完全隔离。