ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux --线程概念与控制

Linux --线程概念与控制 什么是线程线程的朴素理解线程最直观的理解就是一个程序内部的一条执行路线。你可以把进程想象成一个工厂线程就是工厂里的生产线。一个工厂至少有一条生产线主线程也可以有多条生产线同时工作多线程。更精确的定义是线程是进程内部的一个控制序列。Linux视角下的线程在Linux系统中线程在CPU眼中其实就是一个PCB进程控制块。内核并不区分进程和线程它们都被抽象为task_struct结构体。区别在于进程拥有独立的虚拟地址空间线程共享同一个进程的虚拟地址空间这就是为什么说线程是轻量级进程——创建线程不需要复制整个地址空间只需要在已有空间里划出一块区域给新线程用。虚拟地址空间与分页要真正理解线程必须先搞懂虚拟地址空间和分页机制。为什么要虚拟地址想象你是一个大型图书馆的管理员而正在运行的程序如浏览器、游戏是借阅者。物理内存RAM是真实书架空间有限且位置固定0号架、1号架...。直接使用物理地址的灾难冲突如果程序A占用了0-100号架程序B也想用0号架系统立刻崩溃。碎片化程序运行完后释放空间书架出现很多空洞新来的大程序可能塞不进去。安全漏洞程序A可以随意读取程序B书架上的书数据。编译噩梦程序员在写代码时必须硬编码“我的变量放在物理内存的第1024号格子”这完全不可行。解决方案给每个“借阅者”发一张专属地图虚拟地址空间。在这张地图上每个程序都认为自己拥有从0x0000到0xFFFF的整栋图书馆完整地址空间。管理员操作系统硬件负责在背后把“地图上的书架号”映射到“真实物理书架号”。分页Paging—— 映射的核心机制既然虚拟地址这么大物理内存比如8GB这么小如何映射核心思想分而治之按“页”映射按需调入。1. 基本单位页Page虚拟地址空间被切成固定大小的块通常为4KB也可配置为2MB或1GB大页。页框Page Frame物理内存被切成同样大小的块4KB。映射关系虚拟页号VPN → 物理页框号PPN。虚拟地址空间连续 物理内存离散 ┌─────────────┐ ┌─────────────┐ │ 虚拟页 0 │───────┐ │ 页框 5 │ ├─────────────┤ ├──▶├─────────────┤ │ 虚拟页 1 │───┐ │ │ 页框 2 │ ├─────────────┤ │ └──▶├─────────────┤ │ 虚拟页 2 │ │ │ 页框 8 │ ├─────────────┤ └──────▶├─────────────┤ │ 虚拟页 3 │ │ 页框 1 │ └─────────────┘ └─────────────┘关键点虚拟地址是连续的物理地址可以是离散的。程序只需要连续的逻辑地址剩下的由硬件MMU和操作系统搞定。页表Page Table—— 映射字典每个进程都有一个页表保存在物理内存中。页表就像一个字典记录了该进程每个虚拟页对应哪个物理页框。页表的每一项称为页表项PTEPage Table Entry。PTE的核心字段就是页框号PFNPage Frame Number。页表项PTE的关键字段物理页框号指向真实内存地址。有效位该页是否在物理内存中如果为0访问会触发缺页中断。读/写/执行权限控制访问保护如代码段不可写。脏位该页是否被修改过用于换出时判断是否需要写回磁盘。访问位用于内存淘汰算法LRU。工作机制CPU发出一个虚拟地址例如0x0000_7FFF_FFFF_0123。MMU内存管理单元提取出虚拟页号VPN。用VPN作为索引在页表中查表找到对应的PTE。从PTE中读出物理页框号PFN。将PFN与虚拟地址的页内偏移低12位拼接形成最终的物理地址访问RAM。多级页表解决空间爆炸问题如果使用单级页表64位系统有2^64 / 2^12 2^52个页表项每个页表项8字节单个进程页表需要32PB内存——根本不现实。解决方案多级页表如x86-64的4级页表虚拟地址被拆分成4个索引字段 页内偏移。每一级页表都是一个数组指向下一级页表的物理地址。巨大优势如果某个中间级的页表不存在进程没使用该区域就不分配下一级页表极大地节省内存。CR3寄存器 ──▶ 页目录1024项 ├──▶ 页表11024项──▶ 物理页 ├──▶ 页表21024项──▶ 物理页 └──▶ 页表31024项──▶ 物理页一个程序可能只需要几十个页表就能覆盖全部内存需求而不用一次性分配全部4MB。我们以一个进程从诞生到运行的过程为例1. 进程创建fork/exec内核为新进程分配一个mm_struct内存描述符。内核分配1 个物理页框作为PML4表顶级页表将其物理地址写入CR3寄存器。PML4表中的所有条目初始化为“无效Present0”。此时只占用 1 个页框4KB。2. 加载可执行文件exec内核读取ELF文件的段代码段、数据段在虚拟地址空间中建立VMA虚拟内存区域。但此时并不分配下级页表只是记录“将来需要映射哪些虚拟地址范围”。3. 程序开始执行访问虚拟地址当CPU执行第一条指令访问虚拟地址0x400000时MMU查PML4表 → 对应的PML4条目无效 → 触发缺页中断。内核检查该虚拟地址是否在VMA中合法。如果合法内核分配 1 个物理页框作为PDP表并将该页框的PFN填入PML4条目中置有效位1。继续查PDP表 → 对应条目无效 → 再分配PD表。继续查PD表 → 对应条目无效 → 再分配PT表。继续查PT表 → 对应条目无效 → 分配数据页框从磁盘加载代码/数据填入PTE。此时这条访问路径上分配了 PML4 PDP PD PT 数据页 5 个物理页框20KB。4. 访问另一个不同区域的地址如栈0x7FFF_FFFF_0000PML4表已经存在无需再分配。查PML4表找到对应的条目可能索引不同→ 如果无效分配新的PDP表另一个物理页框。继续分配新的PD表、新的PT表、新的数据页框。地址转换流程MMU的工作以32位系统为例虚拟地址被分成三部分高10位页目录索引中10位页表索引低12位页内偏移虚拟地址: [页目录索引(10bit)] [页表索引(10bit)] [偏移(12bit)] ↓ ↓ ↓ 查页目录表 查页表 物理页内偏移 ↓ ↓ ↓ 得到页表地址 得到物理页地址 ────┘ ↓ 物理地址 物理页地址 偏移MMU内存管理单元是硬件电路专门做这件事速度极快。TLB——加速地址转换的缓存多级页表节省了内存但带来了一个新问题地址转换变慢了。单级页表1 次内存访问查页表。4级页表需要4 次内存访问x86-64 4级分页下查 PML4、PDP、PD、PT 1 次数据访问 5 次内存访问。硬件救星TLBTranslation Lookaside Buffer快表TLB 是 CPU 内部的一个小型硬件缓存专门缓存最近用过的“虚拟页号 → 物理页框号”映射。如果 TLB 命中地址转换在1 个时钟周期内完成无需遍历多级页表。只有当 TLB 未命中时CPU 才“慢速”遍历 4 级页表约需几十到几百个时钟周期并将结果存入 TLB。缺页异常Page Fault当CPU访问的虚拟地址在TLB和页表中都找不到对应物理页时触发缺页异常。缺页异常分三种类型含义处理方式Hard Page Fault硬缺页物理内存中没有对应页从磁盘读取到内存Soft Page Fault软缺页物理内存有但映射关系未建立只需建立映射Invalid Page Fault无效缺页访问了非法地址触发段错误进程崩溃思考malloc和new分配内存时真的立刻就分配物理内存了吗答案不是的malloc只是在虚拟地址空间中划了一块区域真正分配物理内存是在第一次访问时触发缺页异常才发生的。这就是惰性分配Lazy Allocation。同一进程下的两个线程映射物理内存的全过程第一部分场景设定系统32位 x862级页表简化但原理完全一致。进程p已启动PID 100。线程main主线程和worker工作线程。物理内存状态一开始进程刚创建只分配了1 个物理页框4KB作为页目录Page Directory所有条目无效。第二阶段创建线程——内核干了什么当调用pthread_create时内核执行clone(CLONE_VM | CLONE_FILES | ...)不创建新的mm_struct直接将子线程worker的task_struct-mm指针指向主线程的mm_struct。不复制页目录worker 的CR3寄存器值等于main 的CR3值指向同一个物理页框。分配线程栈的虚拟地址在虚拟地址空间中用mmap预留一块区域比如0x7FFF_0000~0x7FFF_FFFF大小4MB插入一个VMA虚拟内存区域。页表状态此时页目录和页表完全没有变化VMA 只是一个“将来要用的规划”。此刻物理内存占用只有1 个页框页目录 线程控制块TCB等内核数据结构没有为线程栈分配任何物理页框。第三阶段主线程访问全局变量共享数据主线程执行int g_val 42;全局变量虚拟地址0x0001_0000第1步CPU 的 MMU 查页表虚拟地址0x0001_0000高20位VPN0x00010中10位PT索引0x000低12位偏移0x000。CR3 → 页目录物理地址假设0x2000_0000。页目录条目PD[0x00010]因为VPN0x00010→ 发现present 0无效。第2步触发缺页中断#PFCPU 保存上下文跳转到内核的do_page_fault()。内核检查0x0001_0000是否在合法的 VMA 中 → 是全局数据段。内核执行handle_mm_fault()a. 分配页表PT页框从伙伴系统分配 1 个空闲物理页框假设 PFN 0x100物理地址0x1000_0000。在页目录中将PD[0x00010]填入该页框的物理地址0x1000_0000置present1。b. 分配数据页框再分配 1 个物理页框假设 PFN 0x200物理地址0x2000_0000。在页表位于0x1000_0000中用中10位索引0x000找到 PTE填入 PFN0x200置present1。c. 从磁盘加载数据将可执行文件中.data段对应的 4KB 数据g_val42读入物理页框0x2000_0000。第3步返回用户态重试指令MMU 再次查表这回页目录→页表→PTE 全部有效得到物理地址0x2000_0000 偏移0 → 成功读取42。同时CPU 自动将 VPN0x00010→ PFN0x200的映射存入 TLB。此时物理内存页目录1页框 页表1页框 数据页1页框3 个页框12KB。第四阶段工作线程worker访问同一个全局变量工作线程执行printf(%d, g_val);访问0x0001_0000第1步MMU 查 TLB由于主线程刚刚访问过TLB 中已经有了0x00010→0x2000_0000的缓存。TLB 命中CPU 直接得到物理地址0x2000_0000完全不需要遍历页表。关键点Worker 和 Main 共享同一个虚拟地址0x0001_0000。它们共享同一份页表因为 CR3 相同所以 TLB 缓存的映射对 worker 同样有效。最终访问的是同一个物理页框0x2000_0000→ 读到相同的42。物理内存无变化因为页表已经存在数据页已经分配。第五阶段工作线程写自己的栈私有数据工作线程执行int local 100;栈上分配虚拟地址0x7FFF_0000第1步MMU 查页表虚拟地址0x7FFF_0000高20位VPN0x7FFF0中10位PT索引0x000偏移0。CR3 → 页目录0x2000_0000→ 查PD[0x7FFF0]→ 发现present0因为主线程从未访问过这个区域。第2步触发缺页中断内核检查 VMA发现0x7FFF_0000属于worker 的栈 VMA。内核执行缺页处理a. 分配新的页表PT页框从伙伴系统分配另一个空闲物理页框假设 PFN 0x300物理地址0x3000_0000。在页目录中将PD[0x7FFF0]填入0x3000_0000置present1。注意这个页表是新的与主线程用的页表PFN0x100不同。b. 分配栈数据页框再分配一个物理页框假设 PFN 0x400物理地址0x4000_0000。在新的页表位于0x3000_0000中用中10位索引0x000找到 PTE填入 PFN0x400置present1。将该页框内容清零栈初始化。第3步MMU 重试页目录→页表→PTE 全部有效得到物理地址0x4000_0000写入100。此时物理内存页目录1 页框保持不变主线程数据页表1 页框PFN0x100主线程数据页1 页框PFN0x200工作线程栈页表1 页框PFN0x300工作线程栈数据页1 页框PFN0x400总共 5 个页框20KB全都通过共享的页目录被索引到。线程与进程的关系——共享与独有一句话总结进程是资源分配的基本单位线程是CPU调度的基本单位线程之间共享什么同一进程内的所有线程共享虚拟地址空间代码段、数据段、堆文件描述符表信号处理方式当前工作目录用户ID和组ID线程独有什么每个线程拥有自己的线程ID寄存器上下文PC、SP等栈栈空间独立errno变量信号屏蔽字调度优先级重要图示进程地址空间 ┌────────────────────────────────┐ │ 内核空间 │ ← 所有线程共享 ├────────────────────────────────┤ │ 栈 │ ← 主线程的栈 ├────────────────────────────────┤ │ ↓ │ │ ↑ │ ├────────────────────────────────┤ │ 堆 │ ← 所有线程共享 ├────────────────────────────────┤ │ 共享库/映射区 │ ← 子线程的栈在这里 ├────────────────────────────────┤ │ 数据段(.data) │ ← 所有线程共享 ├────────────────────────────────┤ │ 代码段(.text) │ ← 所有线程共享 └────────────────────────────────┘关键理解子线程的栈为什么在共享区因为pthread_create内部通过mmap在共享区堆和栈之间的区域申请了一块匿名内存作为线程栈。而主线程的栈在虚拟地址空间的栈区。线程的优缺点线程的核心优势线程最本质的特点是轻量级和共享性这带来了四个决定性的优势1. 创建和切换开销极低速度优势创建创建一个进程需要复制父进程的页表、文件描述符表、环境变量等即使使用fork()COW也要复制页表。而创建一个线程只需分配一个线程控制块TCB和一个内核栈几乎不涉及内存管理单元的TLB刷新。切换上下文切换切换进程时CPU需要切换虚拟地址空间CR3寄存器导致TLB全部失效成本极高。而切换线程时虚拟地址空间保持不变因为属于同一进程只需切换寄存器和程序计数器开销小一个数量级。量化对比在Linux上进程切换耗时约1~2微秒线程切换约0.1~0.3微秒。2. 高效的数据共享通信优势进程间通信IPC管道、消息队列、共享内存需要经过内核涉及数据拷贝和同步复杂且慢。线程间通信由于所有线程共享进程的堆Heap和全局数据段它们可以直接通过指针读写同一块内存。效果数据传递是零拷贝的延迟接近内存访问速度这是高性能服务器如Nginx、Redis采用多线程或多进程配合共享内存的根本原因。3. 充分利用多核CPU并行优势单线程程序在一个时刻只能运行在一个CPU核心上。多线程可以将任务拆解如并行计算、同时处理多个网络请求让多个核心同时工作。4. 编程模型的便利性I/O阻塞场景对于需要同时处理多个I/O操作如网络读写、文件读写的程序使用多线程可以将每个请求分配给独立线程。当一个线程因I/O阻塞时CPU可以立即切换到其他就绪线程从而提高CPU利用率相比单线程轮询编程逻辑更直观。线程的致命缺点共享是一把双刃剑。线程的缺点几乎全部源于资源共享带来的副作用。1. 数据竞争Race Condition与同步噩梦最大痛点问题两个线程同时修改同一个全局变量如count在汇编层面需要“读-改-写”三步。如果线程A读到5线程B也读到5A写回6B也写回6最终结果是6而非7。代价为了修复这个问题必须引入互斥锁Mutex、读写锁RWLock、信号量Semaphore。副作用性能下降加锁解锁本身有开销且导致CPU缓存行Cache Line频繁失效。死锁Deadlock线程A持有锁1等待锁2线程B持有锁2等待锁1两败俱伤。优先级反转低优先级线程持锁高优先级线程等待导致实时性崩溃。编程复杂度飙升编写正确且高性能的并发代码难度远超单线程。2. 健壮性极差一个线程崩溃全家陪葬进程隔离进程A访问非法内存如空指针操作系统回收进程A的物理内存进程B毫发无损。线程隔离所有线程共享同一虚拟地址空间。如果线程1解引用空指针触发Segmentation Fault操作系统会直接杀死整个进程所有线程瞬间消亡。3. 调试与复现极其困难单线程程序错误是确定性的输入相同输出相同。多线程的Bug特别是死锁和竞态条件往往依赖于CPU指令执行的交错时序这在毫秒级甚至纳秒级变化。可能运行1000次才出现一次且难以通过日志还原现场。4. 资源消耗并非“零成本”每个线程都有独立的栈空间默认Linux下8MB。创建1000个线程就需要约8GB的虚拟地址空间虽然物理内存按需分配但虚拟地址空间本身是有限的尤其在32位系统。调度开销当线程数量远大于CPU核心数如1000个线程争抢8个核操作系统调度器频繁进行上下文切换开销会非线性增长。5. 无法利用多机分布式资源线程受限于单台物理机的CPU和内存上限。如果需要处理海量数据如PB级必须使用多进程或分布式架构。线程控制——创建、终止、等待POSIX线程库Linux的线程操作遵循POSIX标准使用pthread库#include pthread.h // 编译时链接-lpthread所有函数名以pthread_开头。创建线程 - pthread_createint pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);thread输出参数返回线程IDpthread库维护的attr线程属性NULL表示默认start_routine线程执行的函数arg传递给函数的参数错误检查方式不同传统系统调用返回-1设置errnopthread函数直接返回错误码示例void* thread_func(void* arg) { printf(Hello from thread!\n); return NULL; } int main() { pthread_t tid; int ret pthread_create(tid, NULL, thread_func, NULL); if (ret ! 0) { fprintf(stderr, pthread_create: %s\n, strerror(ret)); exit(1); } // ... }两种线程ID重要概念区分类型来源作用域本质pthread_tpthread_create返回pthread_self()获取进程内唯一虚拟地址空间中的一个地址LWP轻量级进程ID内核分配ps -aL可见系统全局唯一内核task_struct的pidpthread_t本质上是一个指向struct pthread线程控制块TCB的指针这个结构体在堆上分配。当调用pthread_create时glibc 的 pthread 库在堆Heap上分配了一块内存用于存放该线程的控制块——struct pthread。// glibc/nptl/descr.h 简化定义 struct pthread { pid_t tid; // 内核线程ID由内核分配 void *stack; // 线程栈地址 int lock; // 锁 void *(*start_routine)(void*); // 入口函数 // ... 还有大量用于异常处理、TLS、清理函数等的字段 };pthread_self()返回的值本质上是一个指针内存地址指向该线程独有的struct pthread结构体。pthread_t 是“用户态身份证”内核不认TID 是“内核态身份证”用户态很少直接操作。两种线程ID分别属于用户态和内核态两个层面。用户态线程IDpthread_t由 glibc 的 pthread 库管理本质是一个指向struct pthread结构体的指针或编码值作用域仅限当前进程用于pthread_join、pthread_mutex_lock等用户态同步操作。内核态线程IDTID由 Linux 内核分配存储在task_struct-pid中系统全局唯一用于内核调度、信号发送、资源统计通过syscall(SYS_gettid)获取。两者通过clone系统调用建立映射pthread 库在创建线程时保存内核返回的 TID到struct pthread中以便在需要时如pthread_kill将用户态 ID 转换为内核态 ID。不能混用例如不能用pthread_t直接调用kill系统调用。查看线程信息ps -aL | grep program_name输出PID LWP TTY TIME CMD 2711838 2711838 pts/0 00:00:00 program ← 主线程PID LWP 2711838 2711839 pts/0 00:00:00 program ← 子线程PID不同PID相同的线程属于同一个进程LWP是内核的线程ID。线程终止三种方式1. 从线程函数returnvoid* thread_func(void* arg) { return (void*)123; // 返回退出码 }主线程不能这样用main返回相当于整个进程exit。2. pthread_exitvoid pthread_exit(void *value_ptr);void* thread_func(void* arg) { int* p malloc(sizeof(int)); *p 123; pthread_exit(p); }value_ptr不能指向局部变量栈上的因为线程退出后栈被销毁。栈被销毁这个说法更严谨的表述是栈上局部变量的生命周期结束了那块内存随时可能被复用或释放你不能再依赖它。但关键问题pthread_exit 的参数什么时候被读取看这段代码void* thread_func(void* arg) { int local 123; pthread_exit(local); // 错误传了局部变量地址 } int main() { pthread_t tid; void* ret; pthread_create(tid, NULL, thread_func, NULL); pthread_join(tid, ret); // 这里才去读 ret 指向的内容 printf(%d\n, *(int*)ret); // 读到的是什么 }时间线是这样的t0: 主线程调用 pthread_create创建子线程 t1: 子线程执行 thread_func t2: 子线程执行 pthread_exit(local) → 把 local 这个地址存到 TCB 的 result 字段里 → 子线程退出栈帧销毁 t3: 主线程 pthread_join 返回 → 主线程拿到 ret local → 主线程去读 *(int*)ret问题出在 t2 和 t3 之间。当子线程执行完pthread_exit后它的栈帧stack frame已经出栈了。那块内存虽然可能还在因为栈空间整体还没释放但这块内存不再被这个线程拥有可能被其他函数调用覆盖。如果线程是 detached 的或者已经被 join 过整个栈空间可能已经被munmap释放访问它就是野指针。所以主线程在 t3 读*(int*)ret时读到的是一块已经失效的内存结果是未定义行为可能读到 123也可能读到垃圾值也可能段错误。一句话记住线程函数的返回值必须放在生命周期长于线程本身的内存里——全局变量或堆内存绝不能是栈上的局部变量。3. pthread_cancelint pthread_cancel(pthread_t thread);pthread_cancel(tid); // 取消另一个线程线程等待 - pthread_join为什么要等待线程已退出的线程占用的资源TCB、栈不会自动释放不等待会造成资源泄漏int pthread_join(pthread_t thread, void **value_ptr);三种退出方式的返回值退出方式value_ptr指向的值return线程函数的返回值pthread_exit传给pthread_exit的参数pthread_cancelPTHREAD_CANCELED常量示例void* thread1(void* arg) { int* p malloc(sizeof(int)); *p 1; return p; } int main() { pthread_t tid; void* ret; pthread_create(tid, NULL, thread1, NULL); pthread_join(tid, ret); printf(返回值: %d\n, *(int*)ret); free(ret); return 0; }分离线程 - pthread_detach如果不需要等待线程的返回值可以让线程自动释放资源int pthread_detach(pthread_t thread);// 线程自己分离 pthread_detach(pthread_self());注意一个线程不能既是joinable又是detached的。深入理解 pthread_t 的本质让我们看一段glibc源码揭开pthread_t的神秘面纱。pthread_create 核心流程// nptl/pthread_create.c int __pthread_create_2_1(...) { // 1. 分配线程控制块TCB和栈空间 struct pthread *pd NULL; int err ALLOCATE_STACK(iattr, pd); // 2. 填充TCB pd-start_routine start_routine; // 要执行的函数 pd-arg arg; // 函数的参数 pd-result NULL; // 存放返回值 // 3. 把TCB地址作为线程ID返回 *newthread (pthread_t)pd; // 本质是一个地址 // 4. 调用clone系统调用创建轻量级进程 err create_thread(pd, ...); }线程控制块 struct pthreadstruct pthread { // ... 大量字段 ... void *(*start_routine)(void *); // 用户函数 void *arg; // 用户参数 void *result; // 返回值join时读取 void *stackblock; // 栈地址 size_t stackblock_size; // 栈大小 pid_t tid; // 内核线程IDLWP pid_t pid; // 所属进程ID // ... };pthread_t就是一个指向struct pthread的指针所以在虚拟地址空间里它看起来是一个很大的数字地址。线程栈的分配// nptl/allocatestack.c static int allocate_stack(...) { // 使用mmap分配匿名内存作为线程栈 mem mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); // TCB放在栈的底部对于向下增长的架构 pd (struct pthread *)((char *)mem size - coloring) - 1; // 记录栈信息 pd-stackblock mem; pd-stackblock_size size; *pdp pd; return 0; }线程栈通过mmap分配在共享区不能动态增长用尽就会栈溢出。clone系统调用——线程的本质Linux线程的底层是clone系统调用。int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...);clone和fork的区别在于flags参数// 创建线程的关键flags int clone_flags CLONE_VM | // 共享地址空间 CLONE_FS | // 共享文件系统信息 CLONE_FILES | // 共享文件描述符 CLONE_SIGNAL | // 共享信号处理 CLONE_SETTLS; // 设置线程本地存储当CLONE_VM标志位被设置时新进程线程和父进程共享同一个虚拟地址空间——这就是线程的本质。线程封装C风格为了方便使用可以将pthread线程封装成C类class Thread { public: using Func std::functionvoid(); Thread(Func func) : _func(func), _joined(true) { _name Thread- std::to_string(_count); _status TSTATUS::NEW; } bool Start() { if (_status TSTATUS::RUNNING) return true; int n pthread_create(_tid, nullptr, Run, this); if (n ! 0) return false; _status TSTATUS::RUNNING; return true; } bool Join() { if (_joined) { int n pthread_join(_tid, nullptr); if (n ! 0) return false; return true; } return false; } void Detach() { if (_status TSTATUS::NEW) { _joined false; } } private: static void* Run(void* obj) { Thread* self static_castThread*(obj); self-_func(); return nullptr; } std::string _name; pthread_t _tid; Func _func; bool _joined; TSTATUS _status; static int _count; };使用void hello() { while (true) { std::cout Hello from thread! std::endl; sleep(1); } } int main() { Thread t1(hello); t1.Start(); t1.Join(); return 0; }核心要点速记Linux不区分进程和线程都用task_struct表示区别在于是否共享地址空间线程共享地址空间、文件描述符、信号处理、工作目录线程独有栈、寄存器上下文、线程ID、errno、信号屏蔽字pthread_t本质指向struct pthreadTCB的指针是一个虚拟地址LWP内核级线程ID通过ps -aL查看子线程栈通过mmap分配在共享区不能动态增长pthread_join的必要性释放TCB和栈资源否则内存泄漏线程底层实现pthread_create→ 分配TCB和栈 →clone系统调用带CLONE_VM标志
返回列表