1. Linux线程概念
1-1 什么是线程
• 在⼀个程序⾥的⼀个执⾏路线就叫做线程(thread)。更准确的定义是:线程是"⼀个进程内部 的控制序列"
• ⼀切进程⾄少都有⼀个执⾏线程
• 线程在进程内部运⾏,本质是在进程地址空间内运⾏
• 在Linux系统中,在CPU眼中,看到的PCB都要⽐传统的进程更加轻量化
• 透过进程虚拟地址空间,可以看到进程的⼤部分资源,将进程资源合理分配给每个执⾏流,就形成了线程执⾏流
1-2 分⻚式存储管理
1-2-1 虚拟地址和⻚表的由来
如果在没有虚拟内存和分⻚机制的情况下,每⼀个⽤⼾程序在物理内存上所对应的空间必 须是连续的,如下图:

因为每⼀个程序的代码、数据⻓度都是不⼀样的,按照这样的映射⽅式,物理内存将会被分割成各种 离散的、⼤⼩不同的块。经过⼀段运⾏时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎⽚的形式存在。
怎么办呢?**我们希望操作系统提供给⽤⼾的空间必须是连续的,但是物理内存最好不要连续。**此时虚 拟内存和分⻚便出现了,如下图所⽰:

把物理内存 按照⼀个固定的⻓度的⻚框进⾏分割,有时叫做物理⻚。每个⻚框包含⼀个物理⻚ (page)。⼀个⻚的⼤⼩等于⻚框的⼤⼩。⼤多数 构⼀般会⽀持 32 位 体系结构⽀持 4KB 的⻚,⽽ 64 位 体系结 8KB 的⻚。
区分⼀⻚和⼀个⻚框是很重要的:
• ⻚框是⼀个存储区域;
• ⽽⻚是⼀个数据块,可以存放在任何⻚框或磁盘中。
有了这种机制,CPU便并⾮是直接访问物理内存地址,⽽是通过虚拟地址空间来间接的访问物理内存 地址。所谓的虚拟地址空间,是操作系统为每⼀个正在执⾏的进程分配的⼀个逻辑地址,在32位机 上,其范围从0~4G-1。 操作系统通过将虚拟地址空间和物理内存地址之间建⽴映射关系,也就是⻚表,这张表上记录了每⼀ 对⻚和⻚框的映射关系,能让CPU间接的访问物理内存地址。
总结⼀下,其思想是将虚拟内存下的逻辑地址空间分为若⼲⻚,将物理内存空间分为若⼲⻚框,通过 ⻚表便能把连续的虚拟内存,映射到若⼲个不连续的物理内存⻚。这样就解决了使⽤连续的物理内存 造成的碎⽚问题
页是虚拟地址空间中的"逻辑块",页框是物理内存中的"实际槽位",二者通过页表一一映射,实现离散存储、消除碎片。
1-2-2 物理内存管理
假设⼀个可⽤的物理内存有 4GB 的空间。按照⼀个⻚框的⼤⼩ 4KB 进⾏划分, 4GB 的空间就是 4GB/4KB = 1048576 个⻚框。有这么多的物理⻚,操作系统肯定是要将其管理起来的,操作系统 需要知道哪些⻚正在被使⽤,哪些⻚空闲等等。
内核⽤ struct page 结构表⽰系统中的每个物理⻚,出于节省内存的考虑, struct page 中使 ⽤了⼤量的联合体union。
cpp
/* include/linux/mm_types.h */
struct page {
/* 原⼦标志,有些情况下会异步更新 */
`unsigned long flags;
union {
struct {
/* 换出⻚列表,例如由zone->lru_lock保护的active_list */
struct list_head lru;
/* 如果最低为为0,则指向inode
* address_space,或为NULL
* 如果⻚映射为匿名内存,最低为置位
* ⽽且该指针指向anon_vma对象
*/
struct address_space* mapping;
/* 在映射内的偏移量 */
pgoff_t index;
/*
* 由映射私有,不透明数据
* 如果设置了PagePrivate,通常⽤于buffer_heads
* 如果设置了PageSwapCache,则⽤于swp_entry_t
* 如果设置了PG_buddy,则⽤于表⽰伙伴系统中的阶
*/
unsigned long private;
};
struct { /* slab, slob and slub */
union {
struct list_head slab_list; /* uses lru */
struct { /* Partial pages */
struct page* next;
#ifdef CONFIG_64BIT
int pages; /* Nr of pages left */
int pobjects; /* Approximate count */
#else
short int pages;
short int pobjects;
#endif
};
};
struct kmem_cache* slab_cache; /* not slob */
/* Double-word boundary */
void* freelist; /* first free object */
union {
void* s_mem; /* slab: first object */
unsigned long counters; /* SLUB */
struct { /* SLUB */
unsigned inuse : 16; /* ⽤于SLUB分配器:对象的数⽬ */
unsigned objects : 15;
unsigned frozen : 1;
};
};
其中⽐较重要的⼏个参数:
-
flags :⽤来存放⻚的状态。这些状态包括⻚是不是脏的,是不是被锁定在内存中等。flag的 每⼀位单独表⽰⼀种状态,所以它⾄少可以同时表⽰出32种不同的状态。这些标志定义在 中。其中⼀些⽐特位⾮常重要,如PG_locked⽤于指定⻚是否锁定, PG_uptodate⽤于表⽰⻚的数据已经从块设备读取并且没有出现错误。
-
_mapcount :表⽰在⻚表中有多少项指向该⻚,也就是这⼀⻚被引⽤了多少次。当计数值变 为-1时,就说明当前内核并没有引⽤这⼀⻚,于是在新的分配中就可以使⽤它。
-
virtual:是⻚的虚拟地址。通常情况下,它就是⻚在虚拟内存中的地址。有些内存(即所谓 的⾼端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为NULL,需要的 时候,必须动态地映射这些⻚。
要注意的是 struct page 与物理⻚相关,⽽并⾮与虚拟⻚相关。⽽系统中的每个物理⻚都要分配⼀ 个这样的结构体,让我们来算算对所有这些⻚都这么做,到底要消耗掉多少内存。 算 struct page 占40个字节的内存吧,假定系统的物理⻚为 那么系统中共有⻚⾯ 4KB ⼤⼩,系统有 4GB 物理内存。 1048576 个(1兆个),所以描述这么多⻚⾯的page结构体消耗的内存只不过 40MB ,相对系统 4GB 内存⽽⾔,仅是很⼩的⼀部分罢了。
因此,要管理系统中这么多物理⻚⾯,这 个代价并不算太⼤。 要知道的是,⻚的⼤⼩对于内存利⽤和系统开销来说⾮常重要,⻚太⼤,⻚⻚必然会剩余较⼤不能利 ⽤的空间(⻚内碎⽚)。⻚太⼩,虽然可以减⼩⻚内碎⽚的⼤⼩,但是⻚太多,会使得⻚表太⻓⽽占 ⽤内存,同时系统频繁地进⾏⻚转化,加重系统开销。因此,⻚的⼤⼩应该适中,通常为 比特就业课 512B - 8KB ,windows系统的⻚框⼤⼩为4KB。
1-2-3 ⻚表
⻚表中的每⼀个表项,指向⼀个物理⻚的开始地址。在这是每⼀个⽤⼾程序都拥有的虚拟内存空间。既然需要让 要能够表⽰这所有的 32 位系统中,虚拟内存的最⼤空间是 4GB 空间,那么就⼀共需要 4GB , 4GB 的虚拟内存全部可⽤,那么⻚表中就需 4GB/4KB = 1048576 个表项。如下图所⽰:

虚拟内存看上去被虚线"分割"成⼀个个单元,其实并不是真的分割,虚拟内存仍然是连续的。
这个 虚线的单元仅仅表⽰它与⻚表中每⼀个表项的映射关系,并最终映射到相同⼤⼩的⼀个物理内存⻚ 上。 ⻚表中的物理地址,与物理内存之间,是随机的映射关系,哪⾥可⽤就指向哪⾥(物理⻚)。虽然最终使 ⽤的物理内存是离散的,但是与虚拟内存对应的线性地址是连续的。
处理器在访问数据、获取指令 时,使⽤的都是线性地址,只要它是连续的就可以了,最终都能够通过⻚表找到实际的物理地址。 在 32 位系统中,地址的⻓度是 4 个字节,那么⻚表中的每⼀个表项就是占⽤ 4 个字节。所以⻚表占 据的总空间⼤⼩就是: 1048576*4 = 4MB 的⼤⼩。也就是说映射表⾃⼰本⾝,就要占⽤ 4MB/4KB = 1024 个物理⻚。这会存在哪些问题呢?
• 回想⼀下,当初为什么使⽤⻚表,就是要将进程划分为⼀个个⻚可以不⽤连续的存放在物理内存 中,但是此时⻚表就需要1024个连续的⻚框,似乎和当时的⽬标有点背道⽽驰了......
• 此外,根据局部性原理可知,很多时候进程在⼀段时间内只需要访问某⼏个⻚就可以正常运⾏ 了。因此也没有必要⼀次让所有的物理⻚都常驻内存。
解决需要⼤容量⻚表的最好⽅法是:把⻚表看成普通的⽂件,对它进⾏离散分配,即对⻚表再分⻚, 由此形成多级⻚表的思想。
为了解决这个问题,可以把这个单⼀⻚表拆分成 1024 个体积更⼩的映射表。如下图所⽰。这样⼀ 来,1024(每个表中的表项个数)*1024(表的个数),仍然可以覆盖 4GB 的物理内存空间。

这⾥的每⼀个表,就是真正的⻚表,所以⼀共有 1024 个⻚表,⼀个⻚表⾃⾝占⽤4KB ,⼀共 1024 个⻚表占用 4MB 的物理内存空间,和之前没差别啊? 那
那么 从总数上看是这样,但是⼀个应⽤程序是不可能完全使⽤全部的4GB空间的,也许只要⼏⼗个⻚表就 可以了。例如:⼀个⽤⼾程序的代码段、数据段、栈段,⼀共就需要 10 MB 的空间,那么使⽤ 3 个 ⻚表就⾜够了。
计算过程:
每⼀个⻚表项指向⼀个4KB的物理⻚,那么⼀个⻚表中1024个⻚表项,⼀共能覆盖4MB的物理内 存; 那么10MB的程序,向上对⻬取整之后(4MB的倍数,就是12MB),就需要3个⻚表就可以了。
1-2-4 ⻚⽬录结构
到⽬前为⽌,每⼀个⻚框都被⼀个⻚表中的⼀个表项来指向了,那么这1024 个⻚表也需要被管理起 来。管理⻚表的表称之为⻚⽬录表,形成⼆级⻚表。如下图所⽰:
• 所有⻚表的物理地址被⻚⽬录表项指向
• ⻚⽬录的物理地址被 CR3 寄存器 指向,这个寄存器中,保存了当前正在执⾏任务的⻚⽬录地址。
1-2-5 两级⻚表的地址转换
下⾯以⼀个逻辑地址为例。将逻辑地址( 0000000000,0000000001,11111111111 )转换为物 理地址的过程:
-
在32位处理器中,采⽤4KB的⻚⼤⼩,则虚拟地址中低12位为⻚偏移,剩下⾼20位给⻚表,分成 两级,每个级别占10个bit(10+10)。
-
CR3 寄存器 读取⻚⽬录起始地址,再根据⼀级⻚号查⻚⽬录表,找到下⼀级⻚表在物理内存中 存放位置。
-
根据⼆级⻚号查表,找到最终想要访问的内存块号。 4. 结合⻚内偏移量得到物理地址。

-
注:⼀个物理⻚的地址⼀定是 4KB 对⻬的(最后的 12 位全部为 0 ),所以其实只需要记录物理 ⻚地址的⾼20位即可。
-
以上其实就是MMU的⼯作流程。MMU(MemoryManageUnit)是⼀种硬件电路,其速度很快, 主要⼯作是进⾏内存管理,地址转换只是它承接的业务之⼀。
到这⾥其实还有个问题,MMU要先进⾏两次⻚表查询确定物理地址,在确认了权限等问题后,MMU再 将这个物理地址发送到总线,内存收到之后开始读取对应地址的数据并返回。那么当⻚表变为N级时, 就变成了N次检索+1次读写。可⻅,⻚表级数越多查询的步骤越多,对于CPU来说等待时间越⻓,效率 越低。
让我们现在总结⼀下:单级⻚表对连续内存要求⾼,于是引⼊了多级⻚表,但是多级⻚表也是⼀把双 刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率。
有没有提升效率的办法呢?计算机科学中的所有问题,都可以通过添加一个中间层来解决。MMU引入了新武器,江湖人称快表的TLB(其实,就是缓存)。
当CPU给MMU传新虚拟地址之后,MMU先去问TLB那边有没有,如果有就直接拿到物理地址发到总线给内存,齐活。但TLB容量比较小,难免发生Cache Miss,这时候MMU还有保底的老武器页表,在页表中找到之后MMU除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录一下刷新缓存。

1-2-6 缺⻚异常
设想,CPU给MMU的虚拟地址,在TLB和页表都没有找到对应的物理页,该怎么办呢?其实这就是缺页异常 Page Fault,它是一个由硬件中断触发的、可以由软件逻辑纠正的错误。
假如目标内存页在物理内存中没有对应的物理页,或者存在但无对应权限,CPU就无法获取数据,这种情况下 CPU 就会报告一个缺页错误。
由于 CPU 没有数据就无法进行计算,CPU 罢工了,用户进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler 处理。

缺页中断会交给 Page Fault Handler 处理,其根据缺页中断的不同类型会进行不同的处理:
-
Hard Page Fault 也被称为 Major Page Fault:翻译为硬缺页错误 / 主要缺页错误,这时物理内存中没有对应的物理页,需要 CPU 打开磁盘设备读取到物理内存中,再让 MMU 建立虚拟地址和物理地址的映射。
-
Soft Page Fault ** 也被称为Minor Page Fault:翻译为软缺页错误 / 次要缺页错误,这时物理内存中是存在对应物理页的,只不过可能是其他进程调入的,发出缺页异常的进程不知道而已,此时 MMU 只需要建立映射即可,无需从磁盘读取写入内存,一般出现在多进程共享内存区域。
-
Invalid Page Fault:翻译为无效缺页错误,比如进程访问的内存地址越界访问,又或者对空指针解引用,内核就会报 segment fault 错误,中断进程,直接挂掉。
1-3 线程的优点
-
创建一个新线程的代价要比创建一个新进程小得多。
-
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单地说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB(快表)会被全部刷新,这将导致内存的访问在一段时间内相当低效。但是在线程的切换中,不会出现这个问题,当然还有硬件 cache。
-
线程占用的资源要比进程少很多。
-
能充分利用多处理器的可并行数量。
-
在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务。
-
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
-
I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。
1-4 线程的缺点
• 性能损失
⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计 算密集型线程的数量⽐可⽤的处理器多,那么可能会有较⼤的性能损失,这⾥的性能损失指 的是增加了额外的同步和调度开销,⽽可⽤的资源不变。
• 健壮性降低
编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者 因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护 的。
• 缺乏访问控制
进程是访问控制的基本粒度,在⼀个线程中调⽤某些OS函数会对整个进程造成影响。
• 编程难度提⾼ ◦
编写与调试⼀个多线程程序⽐单线程程序困难得多
1-5 线程异常
• 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
• 线程是进程的执⾏分⽀,线程出异常,就类似进程出异常,进⽽触发信号机制,终⽌进程,进程 终⽌,该进程内的所有线程也就随即退出
1-6 线程⽤途
• 合理的使⽤多线程,能提⾼CPU密集型程序的执⾏效率
• 合理的使⽤多线程,能提⾼IO密集型程序的⽤⼾体验(如⽣活中我们⼀边写代码⼀边下载开发⼯ 具,就是多线程运⾏的⼀种表现)
2. Linux进程VS线程
2-1 进程和线程
• 进程是资源分配的基本单位
• 线程是调度的基本单位
• 线程共享进程数据,但也拥有⾃⼰的⼀部分数据:
线程ID ⼀组寄存器 栈 errno 信号屏蔽字 调度优先级
2-2 进程的多个线程共享
同⼀地址空间,因此TextSegment、DataSegment都是共享的,如果定义⼀个函数,在各线程中都可以调 ⽤,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
• ⽂件描述符表 • 每种信号的处理⽅式(SIG_IGN、SIG_DFL或者⾃定义的信号处理函数)
• 当前⼯作⽬录 • ⽤⼾id和组id
进程和线程的关系如下图:

• 如何看待之前学习的单进程?
答:具有⼀个线程执⾏流的进程
3. Linux线程控制
3-1 POSIX线程库
• 与线程有关的函数构成了⼀个完整的系列,绝⼤多数函数的名字都是以"pthread_"打头的
• 要使⽤这些函数库,要通过引⼊头⽂<pthread.h>
• 链接这些线程函数库时要使⽤编译器命令的"-lpthread"选项
3-2 创建线程

cpp
//创建线程演示
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
int i;
for( ; ; ) {
printf("I'am thread 1\n");
sleep(1);
}
}
int main( void )
{
pthread_t tid;
int ret;
if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
fprintf(stderr, "pthread_create : %s\n", strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for(; ; ) {
printf("I'am main thread\n");
sleep(1);
}
}
线程ID
#include
// 获取线程 ID
pthread_t id = pthread_self(void);
打印出来的tid是通过pthread库中有函数 pthread_self 得到的,它返回⼀个pthread_t类型的 变量,指代的是调⽤pthread_self函数的线程的"ID"。 怎么理解这个"ID"呢?
这个"ID"是pthread库给每个线程定义的进程内唯⼀标识,是pthread库 维持的。 由于每个进程有⾃⼰独⽴的内存空间,故此"ID"的作⽤域是进程级⽽⾮系统级(内核不认识)。 其实pthread库也是通过内核提供的系统调⽤(例如clone)来创建线程的,⽽内核会为每个线程创建 系统全局唯⼀的"ID"来唯⼀标识这个线程。
使⽤PS命令查看线程信息:
cpp
ps -aL | head -1 && ps -aL | grep mythread
#PID LWP TTY TIME CMD
#2711838 2711838 pts/235 00:00:00 mythread
#2711838 2711839 pts/235 00:00:00 mythread
#-L 选项:打印线程信息
LWP是什么呢?LWP得到的是真正的线程ID。之前使⽤ pthread_self 得到的这个数实际上是⼀ 个地址,在虚拟地址空间上的⼀个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线 程ID,线程栈,寄存器等属性。
在 ps-aL 得到的线程ID,有⼀个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟 地址空间的栈上,⽽其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库 提供给我们的。⽽pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
3-3 线程终止
如果需要只终⽌某个线程⽽不终⽌整个进程,可以有三种⽅法:
1. 从线程函数return。这种⽅法对主线程不适⽤,从main函数return相当于调⽤exit。
2. 线程可以调⽤pthread_exit终⽌⾃⼰。
3.⼀个线程可以调⽤pthread_cancel终⽌同⼀进程中的另⼀个线程。

注意:pthread_exit 或 return 返回的指针所指向的内存单元必须是全局的或用 malloc 分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出。

3-4线程等待
为什么需要线程等待?
• 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
• 创建新的线程不会复⽤刚才退出线程的地址空间。

调⽤该函数的线程将挂起等待,直到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参数。
cpp
//多线程的创建,终止,等待
#include <iostream>
#include <cstring>
#include <cstdio>
#include <vector>
#include <pthread.h>
#include <unistd.h>
#define MAX_NUM 10
using namespace std;
struct thread_ret
{
public:
int exit_code;
int exit_result;
};//保存线程终止信息
struct thread_data
{
public:
pthread_t id;
char buffer[64];
};//保存线程信息
void *start_routine(void *args)
{
thread_data *name = (thread_data *)args;
for(int i = 0;i<3;i++)
{
printf("我是子线程 name : %s\n",name->buffer);
sleep(2);
}
//delete name;在等待函数中回收
//线程的终止(两种方式) !!! exit(0);不适用线程
//1. return nullptr;
thread_ret *thret = new thread_ret;
/*
thread_ret *thret = new thread_ret;建在堆上
如果thread_ret *thret;是建在栈上return 后会自动销毁
*/
thret->exit_code = 1;
thret->exit_result = 666;
//2.
pthread_exit((void *)thret);
}
int main()
{
//创建多个线程
vector<thread_data*>threads;
for(int i = 0;i<MAX_NUM;i++)
{
thread_data *th = new thread_data;
snprintf(th->buffer,sizeof(th->buffer),"%s,%d","thread",i);
pthread_create(&th->id,nullptr,start_routine,(void *)th);
threads.push_back(th);
}
for(auto &ele:threads)//输出线程信息
{
printf("id:%lu,buffer:%s\n",(unsigned long)ele->id,ele->buffer);
}
for(int i = 0;i<3;i++)
{
printf("我是主线程\n");
sleep(2);
}
//线程等待
for(auto &ele:threads)
{
//int pthread_join(pthread_t thread, void **value_ptr);!!!注意第二个参数的类型
void *ret = nullptr;
pthread_join(ele->id,&ret);//这里ret = (void*)thret
thread_ret *thret = (thread_ret *)ret;
printf("join: exit_code :%d,exit_result:%d\n",(int)thret->exit_code,(int)thret->exit_result);
delete ele;
delete thret;
}
//第三种线程终止方法(这里不在演示)
//pthread_cancle(pthread_t thread);成功返回0否则返回错误码
}
3-5 分离线程
• 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进⾏pthread_join操作,否则 ⽆法释放资源,从⽽造成系统泄漏。
• 如果不关⼼线程的返回值,join是⼀种负担,这个时候,我们可以告诉系统,当线程退出时,⾃ 动释放线程资源
cpp
//函数原型
int pthread_detach(pthread_t thread);
//可以是线程组内其他线程对⽬标线程进⾏分离,也可以是线程⾃⼰分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,⼀个线程不能既是joinable⼜是分离的。
4. 线程ID及进程地址空间布局
• pthread_create函数会产⽣⼀个线程ID,存放在第⼀个参数指向的地址中。该线程ID和前⾯说的 线程ID不是⼀回事。
• 前⾯讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最⼩单位, 所以需要⼀个数值来唯⼀表⽰该线程。
• pthread_create函数第⼀个参数指向⼀个虚拟内存单元,该内存单元的地址即为新创建线程的线 程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
• 线程库NPTL提供了pthread_self函数,可以获得线程⾃⾝的ID:
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux⽬前实现的NPTL实现⽽⾔,pthread_t类 型的线程ID,本质就是⼀个进程地址空间上的⼀个地址。


5. 线程封装
cpp
//Thread.hpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <functional>
#include <cstring>
#include <string.h>
#include<unistd.h>
#include<vector>
namespace PthreadModlue
{
using func_t = std::function<void()>; // func_t是一个返回值为void 函数类型
static int pthreadnum = 0;
class Pthread
{
public:
Pthread(func_t func) : _isdetach(false), _isrunning(false), res(nullptr), _func(func)
{
pthreadnum++;
_name = "pthread" + std::to_string(pthreadnum);
}
~Pthread() {}
static void *rountinue(void *args)
{
Pthread *ret = static_cast<Pthread *>(args);
ret->_func();
ret->_isrunning=false;
pthread_setname_np(ret->_tid,ret->_name.c_str());
return ret;
}
bool Start()
{
if (_isrunning)
return false;
_isrunning=true;
int n = pthread_create(&_tid, nullptr, rountinue, this); // 这里传入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)
{
_isrunning=false;
int n = pthread_cancel(_tid);
if (n == 0)
{
std::cout << _name << "stop success!!!" << std::endl;
return true;
}
else
{
std::cerr << strerror(n) << std::endl;
}
}
return false;
}
void Dedath()
{
if (_isdetach)
return;
if (_isrunning)
{
// 运行时我们要进行直接分离
pthread_detach(_tid);
EnableDertach();
}
// 走到这说明该线程该没运行
// 此时我们只需要改标志位就好了
// 之后在join函数判断该标志为就好了
EnableDertach();
}
bool Join()
{
if (_isdetach)
{
std::cerr << _name<<" " << "alread is detached!!!" << std::endl;
return false;
}
void *ans = nullptr;
int n = pthread_join(_tid, &ans);
if (n == 0)
{
std::cout << _name <<" "<< "join success!!!" << std::endl;
return true;
}
else
{
std::cout << _name <<" "<< "join failure!!!" << std::endl;
std::cout<<strerror(n)<<std::endl;
return false;
}
}
private:
void EnableDertach()
{
std::cout << "pthread is seted joibable!!!" << std::endl;
_isdetach = true;
}
void EnableRunning()
{
if (!_isrunning)
_isrunning = true;
}
private:
pthread_t _tid; // 线程id
std::string _name; // 线程名字
bool _isdetach; // 是否可分离
bool _isrunning; // 是否运行
void *res; // 现返回值写入
func_t _func; // 现场执行的函数
};
}
cpp
//main.cpp
#include <iostream>
#include <unistd.h>
#include "test.hpp"
void hello1()
{
char buffer[64];
pthread_getname_np(pthread_self(), buffer, sizeof(buffer) - 1);
while (true)
{
std::cout << "hello world, " << buffer << std::endl;
sleep(1);
}
}
void hello2()
{
char buffer[64];
pthread_getname_np(pthread_self(), buffer, sizeof(buffer) - 1);
while (true)
{
std::cout << "hello world, " << buffer << std::endl;
sleep(1);
}
}
int main()
{
pthread_setname_np(pthread_self(), "main");
ThreadModule::Thread t1(hello1);
t1.start();
ThreadModule::Thread t2(std::bind(hello2));
t2.start();
t1.join();
t2.join();
return 0;
}