当前位置: 首页 > news >正文

【Linux】深入Linux多线程架构与高性能编程

目录

一、Linux线程概念

1. 什么叫线程?

2. 分页式存储管理

(1)虚拟地址和页表的由来

(2)物理内存管理

(3)页表

(4)页目录结构

(5)两级页表的地址转换

(6)缺页异常

3. 线程的优点

4. 线程的缺点

5. 线程异常

二、Linux进程vs线程

1. 进程和线程

2. 进程的多个线程共享

三、Linux线程控制

1. POSIX线程库

2. 创建线程

3. 线程终止

(1)pthread_exit

(2)pthread_cancal

4. 线程等待

5. 分离线程

四、线程ID及进程地址空间分布

1. 地址空间分布

2. 线程ID的本质:

3. 调用pthread_join理解:

4. 为什么需要独立栈?

5. 内核数据和用户数据怎么联动?

6. 线程局部存储

7. pthread_setname_np

五、线程栈

1. Linux 中进程与线程栈的区别?

2. 进程(主线程)栈的特点

3. 线程栈的特点

六、线程封装


一、Linux线程概念

1. 什么叫线程?

• 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
内核和资源的角度:进程是承担分配系统资源的基本实体,线程是CPU调度的基本单位。
• 一切进程至少都有一个执行线程。
• 线程在进程内部运行,本质是在进程地址空间内运行。线程对资源的划分其实就是对地址空间虚拟地址范围的划分。
• 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化(轻量级进程)。
• 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
• 函数是虚拟地址(逻辑地址)空间的集合。所以让不同线程执行不同代码块的本质就是让线程执行ELF程序的不同函数。
• 多个线程自动的可以访问相同的存储地址空间和文件描述符,强调系统资源共享

2. 分页式存储管理

(1)虚拟地址和页表的由来

在无虚拟内存和分页机制的情况下,每个用户程序在物理内存中必须占用连续的空间。这种内存管理方式称为连续内存分配。如图:

由于程序代码和数据长度不同,物理内存会被分割成大小不一的离散块。经过一段时间运行后,某些程序退出并释放其占用的内存,导致物理内存中出现大量不连续的空闲碎片。这些碎片虽然总容量可能足够,但无法满足新程序所需的连续空间分配要求,造成内存利用率下降。

我们希望,用户程序仍可拥有连续的逻辑地址空间(便于编程和访问),但物理内存可以是非连续的(避免碎片问题,提高利用率)。所以虚拟内存和分页机制应运而生。

• 虚拟内存为每个进程提供独立的连续虚拟地址空间,与物理内存分离。
• 分页机制将虚拟地址空间划分为固定大小的页,物理内存同样划分为页框
• 通过页表实现虚拟页到物理页框的映射,使得连续的虚拟页可以映射到离散的物理页框

将物理内存按固定长度的页框进行分割(称为物理页)。每个页框包含一个物理页,页大小等于页框大小。常见32位体系结构支持4KB页,64位体系结构通常支持8KB页。需区分页和页框:

• 页框是物理内存中的存储区域。
• 页是数据块,可存放于任意页框或磁盘中。

通过这种机制,CPU不再直接访问物理内存地址,而是通过虚拟地址空间间接访问。虚拟地址空间是操作系统为每个正在执行的进程分配的逻辑地址空间(32位系统中范围为0~4GB-1)。

操作系统通过页表建立虚拟地址空间与物理内存地址之间的映射关系。页表记录了每一对虚拟页和物理页框的映射关系,使CPU能间接访问物理内存地址。

总结:该思想将虚拟内存的逻辑地址空间划分为若干页,物理内存空间划分为若干页框。通过页表可将连续的虚拟内存映射到多个不连续的物理内存页框,从而解决连续物理内存分配导致的碎片问题。

(2)物理内存管理

假设可用物理内存为4GB,按页框大小4KB划分,共有4GB/4KB = 1048576个页框。操作系统需管理这些物理页,跟踪哪些页正在使用、哪些页空闲等。

内核使用struct page结构表示每个物理页。为节省内存,该结构中大量使用了联合体(union)。

/* include/linux/mm_types.h */
struct page {/* 原子标志,描述页的状态和属性(如脏、锁定、高端内存等),有些情况下会异步更新 */unsigned long flags;/** 主要的联合体:根据页的用途,使用其中的一个结构。* 例如,如果页是页缓存或匿名内存,则使用第一个匿名结构体。* 如果页用于slab分配器,则使用第二个‘slab’结构体。*/union {/* 用于由文件或匿名内存映射的页 */struct {/* 换出页列表,例如由zone->lru_lock保护的LRU列表(active_list 或 inactive_list) */struct list_head lru;/** 指向地址空间:*   - 如果页是文件页,指向文件的address_space*   - 如果页是匿名页,最低位(LSB)置1,指向anon_vma对象*   - 如果为NULL,则表示不属于任何映射*/struct address_space *mapping;/* 在映射内的偏移量(文件中的页索引或交换条目) */pgoff_t index;/** 由映射私有,不透明数据:*   - 如果设置了PagePrivate,通常用于指向buffer_heads*   - 如果设置了PageSwapCache,则用于swp_entry_t*   - 如果设置了PG_buddy,则用于表⽰伙伴系统中的阶*/unsigned long private;};/* 用于SLAB、SLOB和SLUB分配器(内核对象缓存)的页 */struct {union {/* 用于将slab连接在缓存的全/空列表上(也复用lru字段) */struct list_head slab_list;/* 用于部分分配的slab页面 */struct {struct page *next; /* 指向部分slab列表中的下一页 */
#ifdef CONFIG_64BITint pages;        /* 部分列表中剩余的页数 */int pobjects;     /* slab中大致剩余的对象数 */
#elseshort int pages;short int pobjects;
#endif};};/* 指向这个slab所属的kmem_cache结构体(SLOB分配器不用) */struct kmem_cache *slab_cache;/* 指向slab中第一个空闲对象 */void *freelist;/* 另一个联合体,用于slab分配器的计数器或第一个对象指针 */union {void *s_mem;        /* 指向slab中的第一个对象(SLAB用) */unsigned long counters; /* SLUB用的计数器 */struct {             /* SLUB计数器的位字段形式 */unsigned inuse : 16; /* 已分配对象的数量 */unsigned objects : 15; /* slab中的对象总数 */unsigned frozen : 1;   /* slab是否被冻结(CPU本地) */};};};// ... 此处可能还有用于其他用途(如devmap、hugetlb等)的联合体成员};/** 另一个联合体,通常用于内存管理子系统。* 最常见的是_mapcount,用于跟踪页表映射数量。*/union {atomic_t _mapcount;    /* 页表项映射计数,-1表示无映射,0表示1个,>0表示多个 */unsigned int page_type; /* 标识页的具体类型 */unsigned int active;    /* SLAB分配器使用 */int units;              /* SLOB分配器使用 */};// ... 此处可能还有其他系统特定或内存模型特定的字段#ifdef WANT_PAGE_VIRTUAL/** 内核虚拟地址(如果没有直接映射则为NULL,即高端内存)。* 在32位系统上更常见,64位系统通常所有内存都是直接映射的。*/void *virtual;
#endif /* WANT_PAGE_VIRTUAL */// ... 结构体末尾可能还有其他字段(如_refcount引用计数通常也在附近)
};

其中比较重要的几个参数:

flags :用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。flag的每一位单独表示一种状态,所以它至少可以同时表示出32种不同的状态。这些标志定义在 <linux/page-flags.h> 中。其中一些比特位非常重要,如 PG_locked 用于指定页是否锁定, PG_uptodate 用于表示页的数据已经从块设备读取并且没有出现错误。

_mapcount :表示在页表中有多少项指向该页,也就是这一页被引用了多少次。当计数值变为-1时,就说明当前内核并没有引用这一页,于是在新的分配中就可以使用它。

virtual :是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(即所谓的高端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为 NULL ,需要的时候,必须动态地映射这些页。

要注意的是 struct page 与物理页相关,而并非与虚拟页相关而系统中的每个物理页都要分配一个这样的结构体,让我们来算算对所有页面都这么做,到底要消耗掉多少内存。

算 struct page 占 40 个字节的内存吧,假定系统的物理页为 4KB 大小,系统有 4GB 物理内存。那么系统中共有页面 1048576 个(1兆个),所以描述这么多页面的 page 结构体消耗的内存只不过 40MB ,相对系统 4GB 内存而言,仅是很小的一部分罢了。因此,要管理系统中这么多物理页面,这个代价并不算太大。

要知道的是,页的大小对于内存利用和系统开销来说非常重要,页太大,页必然会剩余较大不能利用的空间(页内碎片)。页太小,虽然可以减小页内碎片的大小,但是页太多,会使得页表太长而占用内存,同时系统频繁地进行页转化,加重系统开销。因此,页的大小应该适中,通常为 512B - 8KB ,windows系统的页框大小为 4KB。

(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 的倍数,就是 12 MB),就需要 3 个页表就可以了。

(4)页目录结构

到目前为止,每一个页框都被一个页表中的一个表项来指向了,那么这 1024 个页表也需要被管理起来。管理页表的表称之为页目录表,形成二级页表。如图:

• 所有页表的物理地址被页目录表项指向
• 页目录的物理地址被 CR3 寄存器 指向,这个寄存器中,保存了当前正在执行任务的页目录地址。

所以操作系统在加载用户程序的时候,不仅仅需要为程序内容来分配物理内存,还需要为用来保存程序的页目录和页表分配物理内存。

(5)两级页表的地址转换

下面以一个逻辑地址为例。将逻辑地址( 0000000000,0000000001,11111111111 )转换为物理地址的过程:

• 在32位处理器中,采用4KB的页大小,则虚拟地址中低12位为页偏移,剩下高20位给页表,分成两级,每个级别占10个bit(10+10)。
• CR3 寄存器 读取页目录起始地址,再根据一级页号查页目录表,找到下一级页表在物理内存中存放位置。
• 根据二级页号查表,找到最终想要访问的内存块号。
• 结合页内偏移量得到物理地址。

• 注:一个物理页的地址一定是 4KB 对齐的(最后的 12 位全部为 0 ),所以其实只需要记录物理页地址的高 20 位即可。
• 以上其实就是 MMU 的工作流程。MMU(Memory Manage Unit)是一种硬件电路,其速度很快,主要工作是进行内存管理,地址转换只是它承接的业务之一。

到这里其实还有个问题,MMU要先进行两次页表查询确定物理地址,在确认了权限等问题后,MMU再将这个物理地址发送到总线,内存收到之后开始读取对应地址的数据并返回。那么当页表变为N级时,就变成了N次检索+1次读写。可见,页表级数越多查询的步骤越多,对于CPU来说等待时间越长,效率越低。

让我们现在总结一下:单级页表对连续内存要求高,于是引入了多级页表,但是多级页表也是一把双刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率。

有没有提升效率的办法呢?计算机科学中的所有问题,都可以通过添加一个中间层来解决。 MMU 引入了新武器,江湖人称快表的 TLB (其实,就是缓存)

当 CPU 给 MMU 传新虚拟地址之后, MMU 先去问 TLB 那边有没有,如果有就直接拿到物理地址发到总线给内存,齐活。但 TLB 容量比较小,难免发生 Cache Miss ,这时候 MMU 还有保底的老武器页表,在页表中找到之后 MMU 除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录一下刷新缓存。

(6)缺页异常

设想,CPU 给 MMU 的虚拟地址,在 TLB 和页表都没有找到对应的物理页,该怎么办呢?其实这就是缺页异常(Page Fault ),它是一个由硬件中断触发的可以由软件逻辑纠正的错误

假如目标内存页在物理内存中没有对应的物理页或者存在但无对应权限,CPU 就无法获取数据,这种情况下CPU就会报告一个缺页错误。由于 CPU 没有数据就无法进行计算,CPU罢工了用户进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler 处理。

缺页中断会交给 PageFaultHandler 处理,其根据缺页中断的不同类型会进行不同的处理:

• Hard Page Fault 也被称为 Major Page Fault ,翻译为硬缺页错误/主要缺页错误,这时物理内存中没有对应的物理页,需要CPU打开磁盘设备读取到物理内存中,再让MMU建立虚拟地址和物理地址的映射。

• Soft Page Fault 也被称为 Minor Page Fault ,翻译为软缺页错误/次要缺页错误,这时物理内存中是存在对应物理页的,只不过可能是其他进程调入的,发出缺页异常的进程不知道而已,此时MMU只需要建立映射即可,无需从磁盘读取写入内存,一般出现在多进程共享内存区域。

• Invalid Page Fault 翻译为无效缺页错误,比如进程访问的内存地址越界访问,又比如对空指针解引用内核就会报 segment fault 错误中断进程直接挂掉。(段错误)

注:写实拷贝、缺页中断、内存申请,背后都可能要重新建立新的页表和建立映射关系操作

理解:

1. 如何理解我们之前学习的new和malloc?

        当你的C程序调用 malloc  或C++程序执行 new 时,在绝大多数现代操作系统此时并不会立即分配物理内存。它会在虚拟内存上申请空间,但页表并不会被修改!操作系统尚未为刚刚分配的虚拟内存分配物理页框,也没有建立映射关系。当你的程序第一次读写刚才 malloc 或 new 返回的指针指向的内存时,CPU会触发缺页中断,操作系统会重新建立映射关系,更新页表。这个过程其实是对物理内存的延迟申请,可以提高内存使用的充分度!

2. 如何理解我们之前学习的写时拷贝?

        写时拷贝的本质是:通过对页框的共享和只在必要时(写入时)进行页框级别的复制,来优化性能和节省资源。

        当父进程调用写时拷贝的 fork() 创建子进程时,内核不会立即复制父进程的物理页框,内核会为子进程创建新的页表,子进程的页表直接指向父进程的物理页框。内核将父进程和子进程的所有页表项都标记为“只读”。当父进程或子进程中的任何一个试图向共享的内存执行写操作时,触发缺页中断,操作系统立刻将原始的共享页框中的数据复制到新的空闲物理页框中,并将权限设置为“可读写”。此后,这两个进程就不再共享这一页数据了。任何一个进程对自己副本的修改,对另一个进程都完全不可见。

3. 如何区分是缺页了,还是真的越界了?
(1)页号合法性检查:操作系统在处理中断或异常的时候,首先检查触发事件的虚拟地址的页号是否合法,如果页号合法但页面不在内存中,则为缺页中断;如果页号非法,则为越界访问。
(2)内存映射检查:操作系统还可以检查触发事件的虚拟地址是否在当前进程的内存映射范围内,如果地址在映射范围内但页面不在内存中,则为缺页中断;如果地址不在映射范围内,则为越界访问。

● 进程资源划分的真相:只要将虚拟地址空间进行划分,进程资源就天然被划分了。

3. 线程的优点

• 创建一个新线程的代价比创建一个新进程小得多。
• 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。

◦ 最主要的区别是线程的切换虚拟内存空间依然是相同的,不用保存CR3寄存器,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

◦ 进程切换的另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲 TLB (快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然进程切换也会导致硬件cache失效,重新缓存。

• 线程占用的资源要比进程少很多。
• 能充分利用多处理器的可并行数量。
• 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
• 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
• I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

4. 线程的缺点

• 性能损失 :一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

• 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

• 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

• 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多。

5. 线程异常

• 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。

• 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

二、Linux进程vs线程

1. 进程和线程

• 进程是资源分配的基本单位
• 线程是调度的基本单位
• 线程共享进程数据,但也拥有自己的一部分“私有”数据:

① 线程ID

一组寄存器,线程的上下文数据:线程有自己独立的PCB(内核)+TCB(用户层.pthread库内部),所以进程是被独立调度的。
栈(动态的):每个线程都有自己的栈,要么是进程自己的,要么是库中创建进程mmap申请的。
④ errno
⑤ 信号屏蔽字
⑥ 调度优先级

2. 进程的多个线程共享

同一地址空间,因此 Text Segment、Data Segment 都是共享的;如果定义一个函数,在各线程中都可以调用;如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:
• 文件描述符表
• 每种信号的处理方式(SIG_IGN、SIG_DFL 或者自定义的信号处理函数)
• 当前工作目录
• 用户 id 和组 id

进程和线程的关系如图:

• 单线程进程就是具有一个线程执行流的进程。

三、Linux线程控制

1. POSIX线程库

• 为什么要建立pthread线程库?

pthread库的建立,是因为Linux内核采用了“轻量级进程”这种独特的方式来模拟线程,但并未向用户提供一套好用的线程操作接口。pthread库通过封装底层的vfork,clone等系统调用,实现了大量用户态的线程管理功能,向上提供了稳定、高效、符合POSIX标准的线程API,使得开发者可以轻松地编写多线程程序,而无需关心底层内核的实现细节。它是在用户层对内核线程(LWP)进行管理的库,但其创建的线程本质上是内核级线程。

• 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的  
• 要使用这些函数库,要通过引入头文 <pthread.h>  
• 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

2. 创建线程

pthread_create 创建一个新的线程,该线程在调用进程中并发执行。新线程从 start_routine 函数开始执行,并接收 arg 作为其唯一参数。

#include <pthread.h>int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);
参数类型描述
threadpthread_t *输出参数。指向线程标识符(TID)的指针,函数成功返回后,这里会存储新线程的ID。
attrconst pthread_attr_t *输入参数。用于设置新线程的属性(如栈大小、调度策略、分离状态等)。如果为 NULL,则使用所有默认属性创建线程。
start_routinevoid *(*)(void*)函数指针。新线程开始执行的函数入口。该函数必须接受一个 void* 参数并返回一个 void* 值。
argvoid *输入参数。传递给 start_routine 函数的参数。它可以是一个基本类型的值(需要强制转换),也可以是一个指向复杂数据结构的指针。

错误检查:

• 传统的一些函数是成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。

• pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。

• pthreads同样提供了线程内的errno变量,以支持其他使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小。

代码验证1:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>void *threadrun(void *args)
{std::string name = (const char*)args;while(true){sleep(1);std::cout << "我是新线程, name: " << name  << "; pid : " << getpid() << std::endl;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");while(true){std::cout << "我是主线程" << ", pid: " << getpid() << std::endl;sleep(1);}return 0;
}
test_thread:TestThread.ccg++ -o $@ $^ -lpthread
.PHONY:clean
clean:rm -f testthread

LWP(light weight process)轻量级进程是真正的线程ID,是 Linux内核分配给线程的内核级标识符。我们看到执行程序时,有两个test_thread线程,它们的PID相同,LWP却不同,所以它们是同一个进程下的不同线程,所以CPU调度时是以LWP为单位。而在【ps -aL】得到的线程ID,有一个线程ID和进程ID相同,这个线程就是主线程。

main函数结束代表主线程结束,一般也代表进程结束。而新线程对应的入口函数运行结束,代表当前线程运行结束。

主线程的栈在虚拟地址空间的栈上,而其他现成的栈是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。

 补充:pthread_self 获取调用线程自身的线程ID

#include <pthread.h>pthread_t pthread_self(void);

代码验证2:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>void showid(pthread_t &tid)
{printf("tid: 0x%lx\n", tid);
}std::string Formattid(const pthread_t &tid)
{char id[64];snprintf(id, sizeof(id), "0x%lx", tid); // 格式化打印return id;
}// 线程执行的函数入口
void *routine(void* args)
{std::string name = static_cast<const char*>(args);pthread_t tid = pthread_self(); // 获得线程idint cnt = 5;while(cnt){std::cout << "我是一个新线程, name: " << name << ", 我的id是: " << Formattid(tid) << std::endl;sleep(1);cnt--;}return nullptr;
}int main()
{pthread_t tid;int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");(void)n;showid(tid);int cnt = 5;while(cnt){sleep(1);std::cout << "我是main线程, name: main thread" << ", 我的id是: " << Formattid(pthread_self()) << std::endl;cnt--;}pthread_join(tid, nullptr); // 等待进程return 0;
}

pthread_t (TID):是 pthread库分配给线程的用户级标识符。它在进程内唯一,用于用户程序的线程管理。主线程与新线程的 pthread_t (TID) 值不同,这确凿地证明了它们是两个独立的、并发执行的用户级线程实体,并且分别对应着两个不同的内核级轻量级进程 (LWP)。

代码验证3:

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>void *threadrun(void *args)
{std::string name = (const char*)args;while(true){sleep(1);std::cout << "我是新线程, name: " << name  << "; pid : " << getpid() << std::endl;int a = 0;a /= 0;}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");while(true){std::cout << "我是主线程" << ", pid: " << getpid() << std::endl;sleep(1);}return 0;
}

在新线程内设置一个异常:任何一个线程崩溃都会导致整个进程崩溃。

代码验证4:

给线程传递的参数和返回值可以是任意类型?是的,线程传递的参数可以是任意类型, 包括自定义对象。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>class Task
{
public:Task(int a, int b):_a(a), _b(b){}int Execute(){return _a + _b;}~Task(){}
private:int _a;int _b;
};class Result
{
public:Result(int result):_result(result){}int GetResult() {return _result;}~Result(){}
private:int _result;
};void *routine(void *args)
{Task *t = static_cast<Task*>(args);sleep(1);Result *res = new Result(t->Execute());sleep(1);return res;
}int main()
{pthread_t tid;Task *t = new Task(10, 20);pthread_create(&tid, nullptr, routine , t); // 线程传递的参数可以是任意类型, 包括自定义对象int cnt = 5;while(cnt--){std::cout << "main线程 " << std::endl;sleep(1);}Result* ret = nullptr;pthread_join(tid, (void**)&ret);int n = ret->GetResult(); std::cout << "新线程结束, 运行结果: " << n << std::endl;return 0;
}
$ make 
g++ -o test_thread TestThread.cc -lpthread
$ ./test_thread
main线程 
main线程 
main线程 
main线程 
main线程 
新线程结束, 运行结果: 30

3. 线程终止

如果进程中的任意线程调用了exit、Exit或者_exit,那么整个进程就会终止。而单线程可以通过3种方式退出,因此可以再不终止整个进程的情况下,停止它的控制流:

1. 线程可以简单地从启动例程中返回 return,返回值是线程的退出码。这种方法对主线程不适用,从main函数 return 相当于调用 exit 。

2. 线程调用pthread_exit终止自己。

3. 线程可以被同一进程中的其他线程取消,通过调用pthread_cancal。一般是main线程取消新线程。取消的时候要保证新线程已经启动了。

(1)pthread_exit

pthread_exit() 函数用于终止调用线程的执行。所有在线程中注册的清理处理程序会被弹出并执行(顺序与注册顺序相反),然后所有线程特定的数据析构函数会被调用。最终,线程停止执行。

#include <pthread.h>void pthread_exit(void *value_ptr);
参数类型说明
value_ptrvoid *线程的退出状态值。与传给启动例程的单个参数类似,该数据可供其他通过 pthread_join 等待此线程结束的线程使用。不能指向一个局部变量

• value_ptr 所指向的数据的生命周期必须独立于线程。它不能是线程栈上的自动变量(局部变量),而应该是:全局变量、静态变量、动态分配的内存(如用 malloc 分配)、一个常量字符串。

• 该函数不会返回。一旦调用,调用它的线程就会立即终止。

(2)pthread_cancal

pthread_cancel() 函数向由 thread 参数指定的线程发送一个取消请求。这是一种异步请求目标线程终止的机制。线程如果被取消,退出结果是 -1 【PTHREAD_CANCAL】。

#include <pthread.h>int pthread_cancel(pthread_t thread);
参数类型说明
threadpthread_t要取消的目标线程的标识符(线程ID)。这个ID通常由 pthread_create 函数创建线程时获得。
返回值说明
0函数调用成功。注意:这仅表示取消请求已成功发送给目标线程,并不保证目标线程已经被取消。
非0函数调用失败,返回值为错误码(如 ESRCH 表示找不到对应的线程)。

代码验证:

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>class Task
{
public:Task(int a, int b):_a(a), _b(b){}int Execute(){return _a + _b;}~Task(){}
private:int _a;int _b;
};class Result
{
public:Result(int result):_result(result){}int GetResult() {return _result;}~Result(){}
private:int _result;
};void *routine(void *args)
{Task *t = static_cast<Task*>(args);sleep(100); Result *res = new Result(t->Execute());sleep(1);// return res;pthread_exit(res); // 进程退出std::cout << "新线程不应该看到这里!" << std::endl;
}int main()
{pthread_t tid;Task *t = new Task(10, 20);pthread_create(&tid, nullptr, routine , t); // 线程传递的参数可以是任意类型, 包括自定义对象sleep(3); // 主线程和新线程谁先启动不确定pthread_cancel(tid); // 在主线程处取消新线程std::cout << "新线程被取消" << std::endl;Result* ret = nullptr;// 使用pthread_join 默认线程无异常pthread_join(tid, (void**)&ret); // join: 拿到的返回值,就是线程退出时设定的返回值std::cout << "新线程结束, 运行结果: " << (long long)ret << std::endl;return 0;
}
$ ./test_thread
新线程被取消
新线程结束, 运行结果: -1

4. 线程等待

为什么需要进程等待?

已经到退出的线程,其占用的栈空间和线程控制块没有被释放,仍在进程的地址空间内。如果不调用 join,这些资源会一直泄漏,直到整个进程结束。

• 创建新的线程不会复用刚才退出线程的地址空间。一个线程(通常是主线程)可能需要等待另一个线程完成特定的工作任务后,才能继续执行后续逻辑。

#include <pthread.h>int pthread_join(pthread_t thread, void **value_ptr);
参数类型描述
threadpthread_t输入参数。要等待的目标线程的标识符(ID)。
value_ptrvoid **输出参数。这是一个指向指针的指针。函数成功返回后,这里会存储目标线程的退出状态(返回值)。如果不对线程的返回值感兴趣,可以传入 NULL

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的。

 线程终止状态:

线程终止方式value_ptr 指向的内容
return value;value (线程函数的返回值)
pthread_exit(value);value (传给 pthread_exit 的参数)
被 pthread_cancel() 取消宏 PTHREAD_CANCELED (一个特殊的 (void *)-1 值)

• 为什么pthread_join没有异常处理的动作?

我们前面知道等待的目标进程如果异常了,整个进程都会退出。所以join异常没有意义。join都是基于线程健康跑完的情况,不需要处理异常信号,异常信号时进程要处理的话题!

代码示例:创建多线程并等待

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>
#include <vector>void *routine(void *args)
{sleep(1);std::string name = static_cast<const char *>(args);delete (char*)args;int cnt = 5;while (cnt--){std::cout << "new线程, name: " << name << std::endl;sleep(1);}return nullptr;
}const int num = 10;int main()
{std::vector<pthread_t> tids;// 1. 创建多线程for (int i = 0; i < num; i++){pthread_t tid;// char id[64]; // 临界区资源(临时空间) —> bugchar *id = new char[64]; // 堆空间snprintf(id, 64, "thread-%d", i);int n = pthread_create(&tid, nullptr, routine, id);if (n == 0)tids.push_back(tid);elsecontinue;// sleep(1);}// 2. 对所有线程进行等待for (int i = 0; i < num; i++){int n = pthread_join(tids[i], nullptr);if (n == 0){std::cout << "等待新线程成功!" << std::endl;}}return 0;
}

5. 分离线程

在默认情况下,新创建的线程是joinable的,线程的终止状态会保存直到对该线程调用 pthread_jion ,来等待它结束并获取其返回值,之后系统才会释放该线程的资源。如果线程已经被分离线程的底层存储资源可以在线程终止时立即被收回。可以调用 pthread_detach 分离线程。

#include <pthread.h>int pthread_detach(pthread_t thread);
参数类型说明
threadpthread_t需要被设置为分离状态(detached)的线程ID。
返回值说明
0函数调用成功。
非0函数调用失败,返回值为错误码(如 EINVAL 表示线程不是可接合状态,或 ESRCH 表示找不到线程)。

• 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。

• 分离的线程依旧在进程的地址空间中,被分离的进程依旧可以访问、操作进程的所有资源。

• 在线程被分离后,我们不能调用 pthread_join 函数等待它的终止状态,因为对分离状态的线程调用 pthread_join 会产生未定义行为(joinable和分离是冲突的,一个线程不能既是joinable又是分离的)。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <cstring>void *routine(void *args)
{// pthread_detach(pthread_self());// std::cout << "新线程被分离" << std::endl;std::string name = static_cast<const char*>(args);int cnt = 3;while(cnt--){std::cout << "新线程, name: " << name << std::endl;sleep(1);    }return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, routine , (void*)"thread-1"); // main线程分离新线程pthread_detach(tid);std::cout << "新线程被分离" << std::endl;int cnt = 5;while(cnt--){std::cout << "main线程" << std::endl;sleep(1);    }int n = pthread_join(tid, nullptr);if(n != 0){std::cout << "pthread_join error" << strerror(n) << std::endl;}else{std::cout << "pthread_join success" << std::endl;}return 0;
}
$ ./test_thread
新线程被分离
新线程, name: thread-1
main线程
新线程, name: thread-1
main线程
新线程, name: thread-1
main线程
main线程
main线程
pthread_join errorInvalid argument

四、线程ID及进程地址空间分布

1. 地址空间分布

Linux操作系统内核并未实现“线程”这一抽象,而是提供了一种更通用的轻量级进程(LWP) 机制。通过LWP,可以创建共享资源(尤其是内存地址空间)的任务,这构成了线程的底层基础。

为了向程序员提供符合POSIX标准的线程编程接口,Glibc中的NPTL(原生线程库) 作为用户态的库,封装了内核的LWP系统调用。当我们编译链接了 pthread 库的程序运行时,通过动态链接和内存映射,pthread 库的代码成为了进程地址空间的一部分,使得进程内的所有线程都可以直接调用它。线程的管理逻辑(库代码)在用户态运行,而线程的实体(执行流)由内核调度。

所以说:进程自己的代码区可以访问到pthread库内部的函数或者数据。

因此,程序中的线程表现为:

• 在内核中:是一个或多个共享同一内存地址空间的LWP(即 task_struct),由内核统一调度。

• 在用户态中:通过 pthread 库提供的API进行创建和管理,库函数负责将高级线程操作转换为具体的LWP系统调用,并管理用户态的线程私有数据。

在Glibc的NPTL实现中,用户态线程的控制结构通常被称为 struct pthread。每个线程的栈(通过 mmap 分配)顶端(通常是栈的低地址端)附近就存放着该线程的 struct pthread 结构体。该结构体存储着线程ID(pthread_t,通常是 struct pthread 的地址)、线程的启动函数和参数、线程的栈起始地址和大小等信息。内核并不知道 struct pthread 的存在。它只认识自己的 task_struct(进程控制块,PCB)。每一个用户态的线程都对应一个内核态的LWP,每个LWP都有一个 task_struct。task_struct 包含内核调度和执行所必需的信息,如:调度信息(优先级、策略、时间片)、硬件上下文(寄存器状态)等。

线程的属性被巧妙地分割了。用户态属性(如线程函数、用户级栈、TLS)由 struct pthread 管理;内核态属性(如调度实体、硬件状态)由 task_struct 管理。两者通过LWP机制一一对应。

2. 线程ID的本质:

pthread_create() 返回的 pthread_t 类型的线程ID,在NPTL实现中,通常就是该线程对应的 struct pthread 结构体在进程地址空间中的起始虚拟地址。

3. 调用pthread_join理解:

当线程的启动函数执行 return 或调用 pthread_exit() 时,线程并不会立刻完全消失。线程库会执行清理操作(如调用线程局部数据的析构函数、清理栈帧等),并将线程的返回值(一个 void* 指针)存储到该线程的 struct pthread 结构体的某个成员中。此时,线程的内核部分(LWP)已经退出,资源被内核回收,但用户态部分(struct pthread 和线程栈)仍然保留在内存中。调用 pthread_join(tid, &retval) 时,你传入的 tid 就是目标线程的 struct pthread 的地址,库函数通过这个地址找到对应的、已退出线程的管理结构,它从该结构中取出之前保存的线程返回值,放入 retval 指向的位置。最关键的一步:然后,它释放这个线程所占用的用户态资源——主要是通过 munmap 释放该线程的栈内存(这块内存也包含了顶端的 struct pthread)。至此,这个线程在用户态的所有痕迹都被彻底清理,避免了内存泄漏。

pthread_join 的核心作用之一是“收尸”。它回收已终止线程的用户态资源(栈和struct pthread),并获取其退出状态。如果线程是“分离(detached)”的,库会在线程结束时自动完成这一清理过程。

4. 为什么需要独立栈?

主线程的栈不是由其管理块分配的。而是在程序启动时由操作系统加载器(Loader)自动分配的,通常位于进程地址空间的某个固定区域。新线程的栈是由 pthread 库在用户态主动分配的。每个线程必须有自己独立的调用栈,用于保存函数调用的返回地址、局部变量、参数等。这样才能保证线程在执行函数调用时不会相互干扰。

5. 内核数据和用户数据怎么联动?

调用pthread_create后,不光要在库中创建线程控制的控制块,还要在内核中,创建轻量级进程,调用系统调用 clone(),并通过其参数(尤其是 flags 和 stack)精确指示内核 :“请创建一个与我现在共享几乎所有资源的新执行流(LWP),并且让它一启动就使用我为你准备的这块栈。”。用户库分配栈,并将栈顶地址通过 clone 系统调用传递给内核。内核信任这个值并将其设置为新LWP的硬件栈指针。用户库提供一个内部启动函数指针给内核,内核负责跳转到它。这个启动函数是用户库代码的一部分,它再跳转到用户提供的线程函数。这又形成了一个从内核到用户库再到用户代码的控制流链条。这就使内核数据和用户数据就联动起

来了。

6. 线程局部存储

线程局部存储是一种特殊的变量存储方式,使用 __thread 关键字修饰的全局变量会在每个线程中拥有独立的副本。当多个线程访问同一个被 __thread 修饰的全局变量时,每个线程实际上操作的是自己独立的变量实例,这些实例位于不同的内存地址。

特性:① 不希望定义的全局变量被其他线程使用,确保数据的线程隔离性;② 其次是线程局部存储只能存储内置类型和部分指针,不能用于复杂的类对象。

#include <pthread.h>
#include <iostream>
#include <string>
#include <unistd.h>// int count = 1; // 全局变量本身就是被两个变量共享的
// 加__thread修饰叫做线程的局部存储, 
// 两个线程访问的不是同一个地址,而是各自的局部存储的地址
__thread int count = 1; 
// 为什么要有局部存储? 
// 1.不想让定义的全局变量被其他线程使用
// 2. 线程局部存储只能存储内置类型和部分指针std::string Addr(int &c)
{char addr[64];snprintf(addr, sizeof(addr), "%p", &c);return addr;
}void *routine1(void *args)
{(void)args;while (true){std::cout << "thread-1, count = " << count << " [我来修改count]" << ", &count: " << Addr(count) << std::endl;count++;sleep(1);}
}void *routine2(void *args)
{(void)args;while (true){sleep(1);std::cout << "thread-2, count = " << count << ", &count: " << Addr(count)<< std::endl;}
}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;
}
$ g++ Thread.cc -lpthread
zyt@VM-24-14-ubuntu:~/linux-log/code_25_9_13$ ./a.out
thread-1, count = 1 [我来修改count], &count: 0x7feffa9b663c
thread-2, count = 1 [我来打印count], &count: 0x7feffa1b563c
thread-1, count = 2 [我来修改count], &count: 0x7feffa9b663c
thread-2, count = 1 [我来打印count], &count: 0x7feffa1b563c
thread-1, count = 3 [我来修改count], &count: 0x7feffa9b663c

两个线程 routine1 和 routine2 虽然都访问名为 count 的变量,但由于使用了 __thread 修饰,每个线程操作的都是自己独立的 count 副本。这可以通过输出的内存地址不同来验证——两个线程看到的 count 变量地址是不同的,证明它们访问的是不同的内存位置。

7. pthread_setname_np

pthread_setname_np为指定线程thread设置一个可读的名称name,成功返回0,失败返回错误码。

pthread_getname_np:获取指定线程的名称。成功返回0,失败返回错误码。

// 设置线程名称
int pthread_setname_np(pthread_t thread, const char *name);// 获取线程名称  
int pthread_getname_np(pthread_t thread, char *name, size_t len);
#include <pthread.h>
#include <iostream>
#include <cstring>
#include <unistd.h>void* worker_thread(void* arg) {// 设置当前线程名称pthread_setname_np(pthread_self(), "WorkerThread");char name[16];pthread_getname_np(pthread_self(), name, sizeof(name));std::cout << "工作线程名称: " << name << std::endl;for (int i = 0; i < 3; i++) {std::cout << "工作线程运行中..." << std::endl;sleep(1);}return nullptr;
}int main() {pthread_t tid;char thread_name[16];// 设置主线程名称pthread_setname_np(pthread_self(), "MainThread");// 创建子线程pthread_create(&tid, nullptr, worker_thread, nullptr);// 获取并显示线程名称pthread_getname_np(pthread_self(), thread_name, sizeof(thread_name));std::cout << "主线程名称: " << thread_name << std::endl;pthread_getname_np(tid, thread_name, sizeof(thread_name));std::cout << "子线程名称: " << thread_name << std::endl;pthread_join(tid, nullptr);return 0;
}

五、线程栈

1. Linux 中进程与线程栈的区别?

虽然 Linux 将线程和进程不加区分地统一到了 task_struct 结构体中,但在对待地址空间的栈管理上仍存在重要区别。

2. 进程(主线程)栈的特点

Linux 进程(即主线程)的栈空间对应 main 函数的栈空间。在 fork 创建进程时,操作系统复制父进程的栈空间地址,采用写时拷贝(Copy-On-Write)机制以及支持动态增长机制。如果栈扩展超出预设上限,将发生栈溢出并触发段错误(系统向进程发送段错误信号)。进程栈是唯一可以访问未映射页面而不立即引发段错误的特殊情况——只有在超出扩展上限时才会报错。

3. 线程栈的特点

对于主线程创建的子线程而言,其栈空间不再是向下增长的,而是事先固定分配的。线程栈通常通过调用 glibc/uclibc 等 pthread 库的 pthread_create 接口创建,位于文件映射区(也称为共享区)。

在 glibc 的 nptl/allocatestack.c 中的 allocate_stack 函数中,可以看到通过 mmap 系统调用创建线程栈:

mem = mmap(NULL, size, prot, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

此调用中的 size 参数获取过程较为复杂,可以手动传入栈大小,也可以使用默认值(通常为 8MB)。关键区别在于这种栈不能动态增长,一旦用尽就会导致问题,这与进程创建的 fork 机制不同。

在 glibc 中通过 mmap 获取栈空间后,底层将调用 sys_clone 系统调用,其中获取了通过 mmap 得到的线程栈指针。

int sys_clone(struct pt_regs *regs)
{unsigned long clone_flags;unsigned long newsp;int __user *parent_tidptr, *child_tidptr;clone_flags = regs->bx;// 获取了mmap得到的线程的stack指针newsp = regs->cx;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);
}

因此,子线程的栈实际上是在进程地址空间中映射出来的一块内存区域,原则上是线程私有的。但由于同一个进程的所有线程在创建时会浅拷贝创建者的 task_struct 的很多字段,如果其他线程有意访问,仍然能够访问到该栈空间,这一点需要特别注意。

六、线程封装

// Thread.hpp
#ifndef _THREAD_H_
#define _THREAD_H_#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cstring>
#include <cstdio>namespace ThreadModlue
{// 原子计数器 —— bugstatic uint32_t number = 1;template<typename T>class Thread{// 线程要执行的外部方法,我们不考虑传参,后续有std::bind来进行类间耦合using func_t = std::function<void(T)>;private:void EnableDetach() // 启用分离操作{std::cout << _name << " 线程被分离了" << std::endl;_isdetach = true;}void EnableRunning(){_isrunning = true;}// Routine属于类内的成员函数,默认传参this指针// 设置static就不传this指针了static void *Routine(void *args){Thread<T> *self = static_cast<Thread<T> *>(args);self->EnableRunning();if (self->_isdetach)self->Detach(); // 分离线程self->_func(self->_data);      // 回调处理return nullptr;}public:Thread(func_t func, T data): _tid(0),_isdetach(false), // 默认不是分离状态_isrunning(false),_res(nullptr),_func(func),_data(data){_name = "thread-" + std::to_string(number++);}void Detach(){if (_isdetach)return;if (_isrunning) // 运行中调用detach进行分离pthread_detach(_tid);EnableDetach(); // 如果没有运行直接对标志位置1}bool Start(){if (_isrunning)return false;int n = pthread_create(&_tid, nullptr, Routine, this); // 传入this指针实现回调机制if (n != 0){std::cerr << "Create thread error!" << 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 << "Cancel thread error!" << std::endl;return false;}else{_isrunning = false;std::cout << _name << " stop success!" << std::endl;}}return true;}void Join(){if (_isdetach){std::cout << _name << " 线程已经是分离得了, 不能进行join!" << std::endl;return;}int n = pthread_join(_tid, &_res);if (n != 0){std::cerr << "Join thread error!" << std::endl;}else{std::cout << _name << " join success!" << std::endl;}}~Thread(){}private:pthread_t _tid;std::string _name;bool _isdetach;bool _isrunning;void *_res;func_t _func;T _data;};
}#endif
// Main.cc
#include "Thread.hpp"
#include <unistd.h>
using namespace ThreadModlue;// 传递对象
class ThreadData
{
public:pthread_t tid;std::string name;
};void Count(ThreadData td)
{while (true){std::cout << "我是一个新线程" << std::endl;sleep(1);}
}int main()
{// // 以面向对象的方式封装线程// // Lambda表达式// Thread t([](){//     while(true)//     {//         std::cout << "我是一个新线程" << std::endl;//         sleep(1);//     }// });// t.Start();// t.Detach();// sleep(5);// t.Stop();// sleep(5);// t.Join();///////////////////////////////////////////////////////////ThreadData td;Thread<ThreadData> t(Count, td);t.Start();t.Join();return 0;
}
$ make
g++ -o thread Main.cc -lpthread
$ ./thread
thread-1 create success!
我是一个新线程
我是一个新线程
我是一个新线程
我是一个新线程
我是一个新线程
thread-1 join success!

文章转载自:

http://dtVRK6xC.npkLq.cn
http://0t3HR7KA.npkLq.cn
http://YNpKYxf4.npkLq.cn
http://zd2iFzDf.npkLq.cn
http://DQ2mbXSC.npkLq.cn
http://853YwY2j.npkLq.cn
http://u9doSlbc.npkLq.cn
http://Py9NJwvo.npkLq.cn
http://OglZKaVW.npkLq.cn
http://CqyfhiH7.npkLq.cn
http://Ho1eFruz.npkLq.cn
http://v9PGaMIp.npkLq.cn
http://vQ0Z5DWV.npkLq.cn
http://NrOJ4jf5.npkLq.cn
http://ZY7S8Lor.npkLq.cn
http://PxIbHDaL.npkLq.cn
http://S4fabFaC.npkLq.cn
http://UEMrQO9V.npkLq.cn
http://DoIOrajJ.npkLq.cn
http://IlZ3sIPE.npkLq.cn
http://wz7v4hk4.npkLq.cn
http://9z4qe194.npkLq.cn
http://VOzrcBxl.npkLq.cn
http://U2qtbDp5.npkLq.cn
http://bVymiHG8.npkLq.cn
http://0wNGM1hR.npkLq.cn
http://jvNbQl8N.npkLq.cn
http://4D9ayzZk.npkLq.cn
http://4sYrFqCO.npkLq.cn
http://cWgk77Ju.npkLq.cn
http://www.dtcms.com/a/382986.html

相关文章:

  • Python爬虫-爬取拉勾网招聘数据
  • Python|Pyppeteer解决Pyppeteer启动后,页面一直显示加载中,并显示转圈卡死的问题(37)
  • C++_STL和数据结构《1》_STL、STL_迭代器、c++中的模版、STL_vecto、列表初始化、三个算法、链表
  • 【计算机网络 | 第16篇】DNS域名工作原理
  • C++算法题中的输入输出形式(I/O)
  • 【算法详解】:编程中的“无限”可能,驾驭超大数的艺术—高精度算法
  • Linux基础开发工具(gcc/g++,yum,vim,make/makefile)
  • NLP:Transformer之多头注意力(特别分享4)
  • arm芯片的功能优化方案
  • 【C++】动态数组vector的使用
  • 软件工程实践三:RESTful API 设计原则
  • [硬件电路-221]:PN结的电阻率是变化的,由无穷大到极小,随着控制电压的变化而变化,不同的电场方向,电阻率的特征也不一样,这正是PN的最有价值的地方。
  • 用户争夺与智能管理:定制开发开源AI智能名片S2B2C商城小程序的战略价值与实践路径
  • 5 遥感与机器学习第三方库安装
  • 告别双系统——WSL2+UBUNTU在WIN上畅游LINUX
  • 【开题答辩全过程】以 SpringBoot的淘宝购物优惠系统的设计与实现为例,包含答辩的问题和答案
  • SpringMVC @RequestMapping的使用演示和细节 详解
  • 后端json数据反序列化枚举类型不匹配的错误
  • 【贪心算法】day10
  • vue动画内置组件
  • 构建完整的RAG生态系统并优化每个组件
  • 20250914-03: Langchain概念:提示模板+少样本提示
  • Java 字符编码问题,怎么优雅地解决?
  • CopyOnWrite
  • 【Ambari监控】监控数据接口查询方法
  • shell 脚本:正则表达式
  • 可调精密稳压器的原理
  • Altium Designer(AD)PCB打孔
  • React 状态管理
  • [Spring Cloud][5] 注册中心详解,CAP 理论,什么是 Eureka