Linux——线程概念与控制

一、线程概念

1.1、从概念角度感性地理解线程

教材上的定义:进程是内核数据结构加上代码和数据 线程是进程内部的一个执行分支 进程和线程都是一个执行流

内核和资源角度的理解:进程是分配系统资源的基本实体 线程是CPU调度的基本单位

回顾之前知识 可以知道进程:

进程访问大部分资源都是通过虚拟地址空间进行访问的 可以说地址空间是一个“窗口” 每一个不同的进程的窗口不同 因为每个进程都有自己的独立的内核数据结构

但是若是创建一个“进程” 它可以共享“窗口”呢 只需要将这个进程的资源分配给不同的task_struct就用 进程创建出了线程了 而分配资源的本质就是给不同的task_struct划分不同的虚拟地址范围 因为都是通过虚拟地址加上页表的转化找到内存中实际的资源的

由上面概念得出初步理解

理解一:Linux线程可以采用进程来模拟

理解二:资源的划分本质上是对虚拟地址空间范围的划分 虚拟地址就是资源的代表

理解三:具体是怎么划分之后让线程可以得到不同的资源的?C语言写的函数在编址之后可以看到汇编代码就是一个地址块 这个地址块是连在一起的 所以函数就是虚拟地址空间的集合 那么只需要让不同的task_struct拿到ELF程序不同函数的起始地址即可 也就是说原来的进程是拿到main函数的起始地址 而现在线程是拿到一个ELF程序不同函数起始地址 这样尽可以并发地去执行一个程序 这也是为什么说线程是CPU调度地基本单位

理解四:我们如何面对过去的进程?

首先进程是内核数据结构加上自己的数据代码 而不是task_struct就是进程

过去的进程只有一个task_struct 实际上这个也是可以看作线程 但是只有一个执行分支 这也叫单进程 以前的进程是内部只有一个线程的进程 现在可以存在多个线程 它们并不冲突而且还是互相补充的

理解五:其他平台例如Windows是怎么设计的?Linux为什么要这么设计?

Windows设计了一个单独的内核数据结构TCB存在于PCB内部 开创新的结构自然这个线程的调度一类的也需要单独设计 具有一定的复杂性

Linux:

进程可以模拟出线程 那么实际上用进程模拟出的线程不需要单独设计内核数据结构一类的 只需要服用进程的相关实现 这样不仅简单而且更加健壮不易出错

这样设计当线程需要被调度时 调度的算法以及结构都没有变化

理解六:Linux线程就是轻量级进程

这里其实可以说明另一个事实 就是操作系统和具体的操作系统(例如Linux)操作系统是一个很广泛的概念并且抽象比如我们在学习学的 它提供了思想  而Linux操作系统是操作系统的其中一种实现 更加具体清晰 它提供了思想的具体实现方案

在Linux系统的角度 线程就是一个执行流 在CPU角度 线程是一个轻量级进程 CPU需要真正进行处理 当处理到线程时 这个工作比进程少 可能这就是站在CPU角度看线程 线程比较轻量的原因

那么执行流是<=进程的 等于的原因是可以是单进程 小于就是有很多个task_struct共享资源

#############################################################################

总结

现在可以理解线程是进程内部的一个执行分支了 因为线程在进程的地址空间内运行

并且观察一下 可以知道进程强调独占(部分共享例如通信)而线程强调共享 部分独占

1.2、从资源划分的角度理性理解线程(虚拟到物理、页表、页表相关的概念、部分内存管理的理解)

1.2.1、4kb

首先需要知道4kb的含义

在磁盘上 文件系统划分磁盘文件是以4kb为一个基本单位的 可执行程序就是文件 文件在磁盘上存储 那么可执行程序存储的时候就是以4kb为单位存储的 无论属性还是内容

在内存上也是4kb的逻辑划分 这是OS划分的

内存上一个4kb叫做页框 磁盘上一个4kb叫做页帧 内存和磁盘是以4kb为单位进行IO的(写实拷贝就算只有一个数据改变 实际上申请的也是一个页框4kb 这样做是为了在空间和效率之间找一个平衡点 类似于SLT)

内存大小假设为4Gb那么 就有4Gb/4Kb(1048576)个页框 操作系统需要管理这些页框 方式是先描述再组织

有一个内核数据结构叫做struct page 就是描述某一个页框的数据结构 但是怎么组织呢 在底层用一个数组来组织 这个数组下标从0到1048575 所以每个页框都有下标  这个结构体里面没有记录自己页框的物理地址 因为不需要记录 每个页框的物理地址就是4kb乘以下标 具体数据的物理地址 只需要拿着数据所在的页框地址加上这个数据在页框中的偏移量即可

1.2.2、申请物理内存具体在做什么

首先查数组找到没有被占用的page 修改page拿去使用 其次如何拿到 每个线程/进程是依靠文件缓冲区来进行数据IO的 每个线程/进程有一个内核数据结构

这是一个基数树 上面的节点指向的就是申请的page页框

这样一来就能申请到物理内存了

1.2.3、页表的具体结构以及如何映射

首先假设虚拟地址和物理地址都是4字节 那么每一个数据就需要用8字节来建立映射关系 物理内存中有4GB的数据 还有其他的数据结构 那么直接这样建立一个页表肯定存不下 

实际上的页表结构

PCB(task_struct)中存放了每一个数据的虚拟地址 其实就在地址空间中 CPU调度时可以拿到这个地址 做转换 转换呢要查页表 页表是怎么一个结构呢

实际上是二级页表 每一个虚拟地址有32位 这32位被划分为3部分 10 10 12 前十个比特位是在第一级页表中也就是页目录 这个页目录存放的是下级页表的地址 总共有1024个 用前10个比特位找到一个页目录对应的下级页表的地址之后 现在看中间10个比特位 这个第二级页表存的是页框的物理地址 每个中有1024个 拿到页框地址就可以找到实际页框了 后12个表示数据的偏移量 是找到页框物理地址之后在页框中找到具体字节的

前面说总共有1048576也就是1024*1024个页框 在这里实际上是一一对应的 因为有1024个页目录 每个页目录又对应1024个页框地址  这个两级页表就算全部使用 也只有4kb*1024大小 何况一个进程不可能全部用到 也就是第二级页表不会申请满 这样就节省了空间

那么CPU要如何拿到页目录的物理地址呢 CPU中集成了一个寄存器CR3 里面保存了当前进程的硬件上下文 通过这个就可以找到页目录的物理地址 之后拿着数据的虚拟地址和CR3里面的页目录地址再通过MMR做上述提到的两级映射工作就可以找到数据物理地址了

这就是转换过程

衍生出来的现象

1、申请物理内存就是查找数组中没有使用的page通过这个page的下标得出需要申请的页框的物理起始地址 之后建立映射填充页表 

2、写实拷贝 缺页中断 内存申请背后可能都需要重新建立页表和新的映射关系 因为可能申请新的page

3、对于进程来说 这也就是一张页目录加上n张页目录构建的映射体系 虚拟地址是索引 物理地址页框是目标 虚拟地址(低12位)加上页框地址 = 物理地址

1.2.4、页表和页表项的源码

* We keep two sets of PTEs - the hardware and the linux version.
* This allows greater flexibility in the way we map the Linux bits
* onto the hardware tables, and allows us to have YOUNG and DIRTY
* bits.
*
* The PTE table pointer refers to the hardware entries; the "Linux"
* entries are stored 1024 bytes below.
*/
// ⻚表标志位
#define L_PTE_PRESENT (1 << 0)
#define L_PTE_FILE (1 << 1) /* only when !PRESENT */
#define L_PTE_YOUNG (1 << 1)
#define L_PTE_BUFFERABLE (1 << 2) /* matches PTE */
#define L_PTE_CACHEABLE (1 << 3) /* matches PTE */
#define L_PTE_USER (1 << 4)
#define L_PTE_WRITE (1 << 5)
#define L_PTE_EXEC (1 << 6)
#define L_PTE_DIRTY (1 << 7)
#define L_PTE_COHERENT (1 << 9) /* I/O coherent (xsc3) */
#define L_PTE_SHARED (1 << 10) /* shared between CPUs (v6) */
#define L_PTE_ASID (1 << 11) /* non-global (use ASID, v6) */
// ⻚表是?
typedef struct { unsigned long pte; } pte_t; // ⻚表项
typedef struct { unsigned long pgd; } pgd_t; // ⻚全局⽬录项
pgd_t *
pgd_alloc(struct mm_struct *mm)
{
pgd_t *ret, *init;
ret = (pgd_t *)__get_free_page(GFP_KERNEL | __GFP_ZERO);
init = pgd_offset(&init_mm, 0UL);
if (ret) {
#ifdef CONFIG_ALPHA_LARGE_VMALLOC
memcpy (ret + USER_PTRS_PER_PGD, init + USER_PTRS_PER_PGD,
(PTRS_PER_PGD - USER_PTRS_PER_PGD - 1)*sizeof(pgd_t));
#else
pgd_val(ret[PTRS_PER_PGD-2]) = pgd_val(init[PTRS_PER_PGD-2]);
#endif
/* The last PGD entry is the VPTB self-map. */
pgd_val(ret[PTRS_PER_PGD-1])
= pte_val(mk_pte(virt_to_page(ret), PAGE_KERNEL));
}
return ret;
}
pte_t *
pte_alloc_one_kernel(struct mm_struct *mm, unsigned long address)
{
pte_t *pte = (pte_t *)__get_free_page(GFP_KERNEL|__GFP_REPEAT|__GFP_ZERO);
return pte;
}
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
unsigned long mmap_base; /* base of mmap area */
unsigned long task_size; /* size of task vm space */
unsigned long cached_hole_size; /* if non-zero, the largest hole
below free_area_cache */
unsigned long free_area_cache; /* first hole of size
cached_hole_size or larger */
pgd_t * pgd; // ⻚⽬录起始地址
}

在内核中页表和页表项实际上就是一个数组 数组中每一个元素都是unsigned long 类型的 

mm_struct里面有一个指针pgd_t* pgd指向页目录的起始地址

 1.3、线程的深刻理解

执行流看到的资源本质上就是在合法的情况下拥有的多少的虚拟地址 也就是说虚拟地址就是资源的代表 虚拟地址空间mm_struct以及vm__area_struct本质就是进行资源的统计数据和整体数据 而页表就是一张虚拟到物理的地图

资源划分本质上就是地址空间划分 资源共享本质上就是地址空间共享

线程进行资源划分本质上是划分地址空间 获得一定范围的合法虚拟地址 再本质上就是对页表的划分

进程进行资源共享本质上是对地址空间的共享 再本质上就是对页表条目的共享

1.4、回归课件

1.4.1、分页式存储管理

为什么要有虚拟地址、两级页表?

思考⼀下,如果在没有虚拟内存和分⻚机制的情况下,每⼀个⽤⼾程序在物理内存上所对应的空间必须是连续的,如下图:
因为每⼀个程序的代码、数据⻓度都是不⼀样的,按照这样的映射⽅式,物理内存将会被分割成各种离散的、⼤⼩不同的块。经过⼀段运⾏时间之后,有些程序会退出,那么它们占据的物理内存空间可以被回收,导致这些物理内存都是以很多碎⽚的形式存在。
举个例子:假设物理内存总容量为 100MB,经过多次分配 / 释放后,空闲内存被分割成:10MB(地址 0-10)、8MB(20-28)、12MB(35-47)、20MB(60-80)—— 总空闲 50MB。此时若有一个进程需要 25MB 连续空间,尽管总空闲足够,但没有任何一块碎片能满足,导致内存分配失败(即使系统还有大量空闲)。
怎么办呢?我们希望操作系统提供给⽤⼾的空间必须是连续的,但是物理内存最好不要连续。此时虚拟内存和分⻚便出现了,如下图所⽰:

其思想是将虚拟内存下的逻辑地址空间分为若⼲⻚,将物理内存空间分为若⼲⻚框,通过
⻚表便能把连续的虚拟内存,映射到若⼲个不连续的物理内存⻚。这样就解决了使⽤连续的物理内存造成的碎⽚问题
页表
⻚表中的物理地址,与物理内存之间,是随机的映射关系,哪⾥可⽤就指向哪⾥(物理⻚)。虽然最终使⽤的物理内存是离散的,但是与虚拟内存对应的线性地址是连续的
单级⻚表对连续内存要求⾼,于是引⼊了多级⻚表,但是多级⻚表也是⼀把双刃剑,在减少连续存储要求且减少存储空间的同时降低了查询效率

1.4.2、TLB

江湖⼈称快表的 TLB (其实,就是缓存,Translation Lookaside Buffer,学名转译后备
缓冲器)
这个就是提升两级页表转换效率的 
CPU MMU 传新虚拟地址之后, MMU 先去问 TLB 那边有没有,如果有就直接拿到物理地址发到总线给内存。但 TLB 容量比较小,难免发⽣ Cache Miss ,这时候 MMU 还有保底的⽼武器
页表,在页表中找到之后 MMU 除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录⼀下刷新缓存。

1.4.3、缺页异常

缺⻚中断会交给 PageFaultHandler 处理,其根据缺⻚中断的不同类型会进⾏不同的处理:
Hard Page Fault 也被称为 Major Page Fault ,翻译为硬缺⻚错误/主要缺⻚错误,这
时物理内存中没有对应的物理⻚,需要CPU打开磁盘设备读取到物理内存中,再让MMU建⽴虚拟
地址和物理地址的映射。
Soft Page Fault 也被称为 Minor Page Fault ,翻译为软缺⻚错误/次要缺⻚错误,这
时物理内存中是存在对应物理⻚的,只不过可能是其他进程调⼊的,发出缺⻚异常的进程不知道
⽽已,此时MMU只需要建⽴映射即可,⽆需从磁盘读取写⼊内存,⼀般出现在多进程共享内存区
域。
Invalid Page Fault 翻译为⽆效缺⻚错误,⽐如进程访问的内存地址越界访问,⼜⽐如对
空指针解引⽤内核就会报 segment fault 错误中断进程直接挂掉。

1.4.4、线程的优缺点

优点

创建⼀个新线程的代价要比创建⼀个新进程小得多
线程占用的资源要比进程少
能充分利⽤多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多

一、进程切换需处理的核心操作(线程切换无需执行)

进程是独立的地址空间单位,切换时操作系统必须完成一系列与地址空间相关的硬性变更:

  1. 地址空间与核心指针切换:需更换当前进程的地址空间标识,修改操作系统中记录当前运行进程的全局指针(如task_current);
  2. 页表与硬件上下文切换:由于不同进程的虚拟地址到物理地址的映射完全独立,必须切换页表,并更新硬件寄存器(如 CR3)中存储的页表基地址,确保 CPU 能正确解析新进程的虚拟地址。

二、性能损耗的核心来源:缓存失效与刷新

上述地址空间相关的操作本身开销有限,进程切换的最大性能损耗来自缓存系统的失效与刷新,而线程切换完全规避了这一点:

  1. TLB(转换检测缓冲区)的失效TLB 是 CPU 用于缓存虚拟地址到物理地址映射关系的硬件缓存,可加速地址解析。进程切换时,页表被完全替换,旧进程的 TLB 缓存对新进程无效,必须清空并重新填充,导致后续地址解析暂时变慢。而线程共享同一进程的地址空间和页表,切换线程时 TLB 缓存依然有效,无需刷新。

  2. CPU 缓存(Cache)的失效CPU 缓存的核心作用是利用数据局部性原理,将频繁访问的内存数据暂存于高速缓存中,减少对慢速物理内存的访问。进程切换后,缓存中存储的是旧进程的数据,对新进程而言多为无效信息(新进程访问的内存区域与旧进程无关),因此需要刷新缓存(或标记为无效),导致新进程初期的内存访问不得不重新从物理内存加载数据,产生性能延迟。而线程属于同一进程,共享内存空间,缓存中存储的仍是当前进程内的有效数据(线程访问的内存区域高度重叠),切换线程时无需刷新缓存,缓存效率得以保留。

线程的缺点
性能损失
⼀个很少被外部事件阻塞的计算密集型线程往往⽆法与其它线程共享同⼀个处理器。如果计
算密集型线程的数量⽐可⽤的处理器多,那么可能会有较⼤的性能损失,这⾥的性能损失指
的是增加了额外的同步和调度开销,⽽可⽤的资源不变。
健壮性降低
编写多线程需要更全⾯更深⼊的考虑,在⼀个多线程程序⾥,因时间分配上的细微偏差或者
因共享了不该共享的变量⽽造成不良影响的可能性是很⼤的,换句话说线程之间是缺乏保护
的。
但是这反过来也是优点 因为缺少保护线程间可以直接通信 不像进程需要做很多工作来看到同一份资源
缺乏访问控制
进程是访问控制的基本粒度,在⼀个线程中调⽤某些OS函数会对整个进程造成影响。
编程难度提⾼
编写与调试⼀个多线程程序⽐单线程程序困难得多

1.4.5、线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执⾏分⽀,线程出异常,就类似进程出异常,进⽽触发信号机制,终⽌进程,进程
终⽌,该进程内的所有线程也就随即退出

二、进程线程共享和独占的资源

进程间具有独⽴性
线程共享地址空间,也就共享进程资源
进程是资源分配的基本单位,线程是调度的基本单位,线程共享进程数据,但也拥有⾃⼰的⼀部分"私有"数据:
线程ID ,⼀组寄存器,线程的上下⽂数据 ,
errno
信号屏蔽字
调度优先级
其中线程独占资源很重要的是线程的上下文数据和独立栈结构 首先说上下文数据 因为线程需要被单独调度所以一定拥有属于自己的上下文数据 栈就是记录自己数据的一个结构也需要独占
进程的多个线程共享
同⼀地址空间,因此 Text Segment Data Segment 都是共享的,如果定义⼀个函数,在各线程中
都可以调⽤,如果定义⼀个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
⽂件描述符表
每种信号的处理⽅式(SIG_ IGN、SIG_ DFL或者⾃定义的信号处理函数)
当前⼯作⽬录
⽤⼾id和组id
进程和线程的关系如下图:

三、Linux线程控制

3.1、代码验证之前的理论

接口介绍 thread_create创建线程 这个并不是系统调用

#include <pthread.h>

       int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

RETURN VALUE
       On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined

第一个参数是一个输出型参数用于存储新创建线程的唯一标识符(线程 ID)。

第二个参数指定新线程的属性(如栈大小、优先级、分离状态等)默认设置为nullptr

第三个参数为新线程的「入口函数」,即线程创建后会自动执行该函数。这个函数我们自己实现

第四个参数传递给线程入口函数 start_routine 的参数。在入口函数内部可以拿到这个参数

#include <iostream>
#include <pthread.h>
#include <unistd.h>


#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)


void* threadrun(void* args)
{
    const char* name = (const char*)args;
    while(true)
    {
        sleep(1);
        std::cout << "这是新线程 ,"<< "name : " << name << std::endl;
    }
    return nullptr;
}
int main()
{
    pthread_t tid = 0;
    // 创建新线程
    int n = pthread_create(&tid, nullptr, threadrun, (void*)"thread-1");
    if(n != 0) 
    {
        ERR_EXIT("pthread_create");
    }
    while(true)
    {
        std::cout << "这是主线程 : " << std::endl;
    }
    return 0;
}
test_thread:testThread.cc
	g++ -o $@ $^ 
.PHONY:clean
clean:
	rm -f test_thread

报错 这是因为找不到库 需要引入第三方库 

修改makefile 为什么需要引入后面说

test_thread:testThread.cc
	g++ -o $@ $^ -pthread
.PHONY:clean
clean:
	rm -f test_thread

运行

回归之前的理论

首先创建新线程之后 入口函数是threadrun 因为一个函数的编址称为一个地址块 那么天然划分为不同区域 这就是资源划分 可以看到这两个线程是并发运行的

看一下运行的时候是有几个进程

只有一个进程

看一下有几个线程 使用命令 ps -aL

PID就是进程id LWP就是轻量级进程id 第一个线程就是主线程和进程id一样  LWP并不是线程id后面会说

可以看到确实有两个线程

LWP:light weight process 轻量级进程 CPU调度的时候看LWP 

细节:

1、调度的时间片是等分给多个线程的 也就是一个进程10个时间片 那么假设有两个线程共享这个进程的地址空间 那么每个线程分到5个时间片

2、之前结论 线程异常的结论 一个线程异常之后所有的线程都退出 这个进程崩溃

这也是为什么线程缺点一点为健壮性相比于进程降低

3、为什么之前打印的时候两个线程的消息是混杂在一起的 这是因为这两个线程都看到的是显示器文件 这属于共享资源 而这个共享资源的写入不是原子性的 没有被保护起来

3.2、引入pthread线程库

为什么在编译的时候需要引入pthread库 

实际上Linux操作系统不存在线程这个概念 只有轻量级进程 他所谓的线程是用进程模拟的 所以Linux只会提供创建轻量级进程的系统调用

而我们用户层只会将线程的概念 为了将用户层和操作系统统一 在这两层之间加入了一个软件层(任何问题都可以通过加入软件层来解决)这个pthread库就在里面 它将LinuxOS的轻量级进程的相关操作封装起来 给用户提供一批使用与线程相关的接口

pthread也叫原生线程库 它和Linux绑定在一起 供给用户层使用

3.3、C++中的多线程

首先来看一份代码

void hello()
{
    while (true)
    {
        std::cout << "新线程 : " << "pid : " << getpid() << std::endl;
        sleep(1);
    }
}
int main()
{
    std::thread t(hello);
    while (true)
    {
        std::cout << "这是主线程 : " << ", pid : " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}
test_thread:testThread.cc
	g++ -o $@ $^ -std=c++11 #-pthread
.PHONY:clean
clean:
	rm -f test_thread

这份代码使用了c++11的线程库

makefile注释掉-pthead 

编译

去掉注释, 编译成功

这是因为Linux下, C++11也是封装了pthread库的 在Windows下C++也封装了它的线程库 

所以语言的可移植性就是大力出奇迹 针对于每个平台都封装

3.3、Linux线程控制的接口

3.3.1、创建&&等待

实际上新线程的退出也需要被等待 否则会出现类似于僵尸进程也就是内存泄漏的问题

等待的接口 pthread_join

       #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);
RETURN VALUE
       On success, pthread_join() returns 0; on error, it returns an error number.

代码

// 线程等待
void* routine(void* args)
{
    std::string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "我是一个新线程, 我的名称 : " << name << std::endl;
        sleep(1); 
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");
    (void)n;

    pthread_join(tid, nullptr);

    return 0;
}

运行结果

主线程在pthread_join处阻塞等待新线程退出 所以运行结果是两个线程一起没有的

#############################################################################

打印tid看是不是LWP

线程id不是LWP 因为Linux底层只有轻量级进程的概念 用户层的线程是封装的 既然是封装就要完整 那么线程id也不用底层的LWP

#############################################################################

观察主线成打印的是不是新线程的tid 这里使用一个接口 获取自己线程的id的接口 pthread_self

       #include <pthread.h>

       pthread_t pthread_self(void);

RETURN VALUE
       This function always succeeds, returning the calling thread's ID.

void showtid(pthread_t & tid)
{
    printf("tid : %ld\n", tid);
}
void* routine(void* args)
{
    std::string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "我是一个新线程, 我的名称 : " << name << ", tid : " << pthread_self() <<std::endl;
        sleep(1); 
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void*)"thread-1");
    (void)n;

    showtid(tid);
    pthread_join(tid, nullptr);

    return 0;
}

#############################################################################

主线程和新线程都可以拿到同一个函数 这是因为这两个线程共享地址空间 并且拿到的这个函数被同时使用不会出错 这是因为这个函数是一个局部函数 可以被重入是一个可重入函数

std::string Formatid(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);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "我是一个新线程, 我的名称 : " << name << ", tid : " << Formatid(pthread_self()) << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");
    (void)n;

    // showtid(tid);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "我是一个main线程" << " , tid : " << Formatid(pthread_self()) << std::endl;
        sleep(1);
    }
    pthread_join(tid, nullptr);

    return 0;
}

这两个线程都是用的Formatid函数 并且不出错

#############################################################################

新线程函数的返回值 这个返回值可以设置 并且可以当作新线程一个暂时的退出码 主线程可以拿到这个暂时的退出码 在pthread_join的第二个参数拿到 这也是为什么第二个参数的参数类型是void**的 这是因为返回值类型是void*的 要拿到这个指针变量的值 要使用其地址传参

void *routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "我是一个新线程, 我的名称 : " << name << ", tid : " << Formatid(pthread_self()) << std::endl;
        sleep(1);
    }
    return (void*)100;
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid, nullptr, routine, (void *)"thread-1");
    (void)n;

    // showtid(tid);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "我是一个main线程" << " , tid : " << Formatid(pthread_self()) << std::endl;
        sleep(1);
    }
    void* ret = nullptr;
    pthread_join(tid, &ret);
    std::cout << "新线程退出码 : " << (long long int)ret << std::endl;

    return 0;
}

这个退出码不能代表任何信息 之前进程的退出码至少表明了退出时退出状态 是正常结束正常退出还是正常结束异常退出或者是异常结束 为什么在join的时候,没有见到异常相关的字段呢??jion都是基于:线程健康跑完的情况,不需要处理异常信号,异常信号,是进程要处理的话题!!!这是因为等待的目标线程,如果异常了,整个进程都退出了,包括main线程,所以,join异常,没有意义,看也看不到!

#############################################################################

main函数结束代表主线程结束 也表示进程结束 ; 新线程对应的入口函数结束 代表当前线程运行结束

#############################################################################

实际上传递给pthread_create的routin函数的参数(也就是pthread_create的最后一个参数)可以是任意类型

代码

// 验证create和join可以传和接收任意参数的类型
class Task
{
public:
    Task(int a, int b) : _a(a), _b(b) {}
    ~Task() {}
    int Execute()
    {
        return _a + _b;
    }

private:
    int _a;
    int _b;
};

class Result
{
public:
    Result(int res) : _res(res) {}
    ~Result() {}
    int Getres()
    {
        return _res;
    }

private:
    int _res;
};

void *routine(void *args)
{
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "这是一个新线程" << std::endl;
        sleep(1);
    }

    Task *t = static_cast<Task *>(args);
    sleep(1);
    Result *res = new Result(t->Execute());
    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;
    pthread_join(tid, (void **)(&ret));
    std::cout << "新线程退出, " << "计算结果为 : " << ret->Getres() << std::endl;
    return 0;
}

3.3.2、终止

1、在当前线程return可以终止

2、使用pthread_exit进行终止 

传递的参数就是和return的值是一样的 

       #include <pthread.h>

       void pthread_exit(void *retval);

RETURN VALUE
       This function does not return to the caller.

void *routine(void *args)
{
    Task *t = static_cast<Task *>(args);
    sleep(1);
    Result *res = new Result(t->Execute());
    pthread_exit(res);
}
int main()
{
    pthread_t tid;
    Task *t = new Task(10, 20);
    pthread_create(&tid, nullptr, routine, t);

    Result *ret;
    pthread_join(tid, (void **)(&ret));
    std::cout << "新线程退出, " << "计算结果为 : " << ret->Getres() << std::endl;
    return 0;
}

3、取消线程

pthread_cancel()

这个函数由主线程调用 取消一个新线程 取消的线程的返回值为-1 也就是PTHREAD_CANCELED

取消的线程必须是启动的

       #include <pthread.h>

       int pthread_cancel(pthread_t thread);

RETURN VALUE
       On success, pthread_cancel() returns 0; on error, it returns a nonzero error number.

注意:线程退出不能用exit 这是进程退出用的 除非特意用这个

#############################################################################

3.3.3、线程分离状态

如果主线程不想再关心新线程,而是当新线程结束的时候,让他自己释放 此时应该怎么做

有一种方法 就是让主线程一直循环 新线程运行的时间短于主线程 那么主线程就不需要等待 此时新线程会被系统回收

第二 设置新线程为分离状态    如何理解分离状态?

技术层面: 线程默认是需要被等待的,joinable。如果不想让主线程等待新线程 想让新线程结束之后,自己退出,设置为分离状态(!joinable or detach)

理解层面:线程分离,主分离新,新把自己分离。分离的线程,依旧在进程的地址空间中,进程的所有资源,被分离的线程,依旧可以访问,可以操作。但是主不再等待新线程  也就是说如果线程被设置为分离状态,不需要进行join,join会失败!!

设置分离状态可以由主线程设置 也可以由新线程自己设置pthread_detach(pthread_self())

pthread_detach

       #include <pthread.h>

       int pthread_detach(pthread_t thread);


RETURN VALUE
       On success, pthread_detach() returns 0; on error, it returns an error number.

代码验证

// 分离状态 主线程不需要等待新线程
void *routine(void *args)
{
    int cnt = 5;
    while (cnt--)
    {
        std::cout << "这是一个新线程" << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, (void *)"thread-1");

    int cnt = 5;
    while (cnt--)
    {
        std::cout << "这是main新线程" << std::endl;
        sleep(1);
    }
    // 线程分离 detach
    pthread_detach(tid);
    // 分离之后主线程不需要等待也不能等待 那么此时join会出错
    void *ret = nullptr;
    int n = pthread_join(tid, &ret);
    if (n != 0)
    {
        std::cout << "pthread join error : " << strerror(n) << std::endl;
    }
    else
    {
        std::cout << "新线程退出, " << "返回值为 : " << (long long)ret << std::endl;
    }
    return 0;
}

说明线程分离之后不能等待 报错是参数不合法 这是因为pthread_join时的tid代表的线程已经被分离了 此时不能等待join

可以分离也侧面说明了线程的退出码其实不重要 出现异常进程结束 主线程收到退出码无意义 新线程代码健康跑完 返回一个返回值 join可以等待 线程分离之后主线程不用等待

3.4、多线程代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <vector>

// 多线程
const int num = 10;
std::vector<pthread_t> tids;


void* routine(void* args)
{
    std::string name = static_cast<const char*>(args);
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "这是一个新线程, name : " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}


int main()
{
    // 创建新线程
    for(int i = 0; i < num; i++)
    {
        char id[64];
        snprintf(id, sizeof id, "thread-%d", i);
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, routine, id);
        if(n == 0) 
            tids.push_back(tid);
        else 
            continue;
    }

    // 等待新线程
    for(int i = 0; i < tids.size(); i++)
    {
        int n = pthread_join(tids[i], nullptr);
        if(n == 0) 
            std::cout << "线程等待成功" << std::endl;
    }
    return 0;
}

若是将sleep(1)放到routine里面会怎么样

依旧成功创建了十个线程 但是每个线程的名字都是9 这是为什么 

因为创建时 循环直接跑完 传给routine的参数是id指针 此时休眠了1s 还没有进行static_cast拷贝 而在这1s内 创建线程的循环跑完 id指针指向的内容是9 那么最后拷贝的都是9了

为了避免这个问题 可以每次都在堆上面申请一个id 让每个新线程的id不一样 当然要释放

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <vector>

// 多线程
const int num = 10;
std::vector<pthread_t> tids;


void* routine(void* args)
{
    sleep(1);
    std::string name = static_cast<const char*>(args);
    delete (char*)args;
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "这是一个新线程, name : " << name << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    // 创建新线程
    for(int i = 0; i < num; i++)
    {
        char* id = new char[64];
        snprintf(id, 64, "thread-%d", i);
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, routine, id);
        if(n == 0) 
            tids.push_back(tid);
        else 
            continue;
    }

    // 等待新线程 一个一个等待
    for(int i = 0; i < tids.size(); i++)
    {
        int n = pthread_join(tids[i], nullptr);
        if(n == 0) 
            std::cout << "线程等待成功" << std::endl;
    }
    return 0;
}

四、线程id及地址空间布局

linux没有真正的线程 它是用轻量级进程模拟的 也就是说OS提供的接口不会是线程的接口 而我们使用的线程接口是封装的轻量级进程接口 形成一个原生库

这个原生库是用户级别的库 是一个共享库

它是可执行文件 也就是ELF格式文件 我们的可执行程序加载 形成进程 动态链接 和动态地址重定向 要将共享库加载到内存中并且映射到进程地址空间的mmap中

也就是说我们的程序在执行时 若是使用到pthread库比如说创建 也会从代码区跳转到共享区访问pthread库内部的函数和数据

那么线程的概念就是在库内部维护的 那么在库内部就一定存在多个被创建的线程 而这个多个线程需要被管理 管理的方式就是先描述再组织 

描述用TCB  里面存放了线程的属性 而优先级时间片上下文等数据不在这里面 因为这涉及到CPU的调度 而OS调度看的是轻量级进程 不是用户层的线程 所以TCB中不存这些

当我们调用pthread_create时 在库内部就会创建管理线程的控制块

那么如何组织呢?

在mmap区用一个数组组织 每个数组的一个位置存在一个线程的管理块 这个控制块中包含但不只包含三个重要信息 struct_pthread就是TCB 线程局部存储 线程栈

tid就是线程在库中对应的管理块的其实虚拟地址 

在TCB中存在一个变量 void*ret 在线程执行完返回时 返回值将记录在自己的ret中 此时该线程的函数释放了 但是呢内存中的数据还没有被释放 这也是为什么需要join的原因 这也是内存泄漏的位置所在 此时join使用这个线程管理块的起始地址也就是第一个参数tid找到要释放的管理块 之后(void**)拿到ret 释放之后返回

LWP和线程id是怎么联系的

首先使用pthread_create时 创建一个线程管理块用来控制线程 其次前面说到 线程是用户层的概念 实际上是封装的Linux底层的轻量级进程 在底层创建轻量级进程的系统调用是clone 这个接口不用过度理解 但是参数是研究这个问题的关键 当pthread_create时 会传入routine入口函数 以及线程栈 routine会传给clone 管理块的线程栈也会传给clone 也就是用户层创建线程实际上只是在进程地址空间申请了自己的管理块 而实际的工作都是由Linux内核完成的 线程栈交给内核完成负责维护函数调用关系、存储局部数据,确保线程能独立、正确地运行 ; routine也是给内核进入 这些给轻量级进程完成是因为底层只认识进程 CPU调度的也是轻量级进程

plus、线程栈

虽然 Linux 将线程和进程不加区分的统⼀到了 task_struct ,但是对待其地址空间的 stack 还是
有些区别的。
对于主线程 线程栈就是main函数栈空间 可以扩容(向下生长)

对于新线程 其栈不再是向下生长的 而是事先固定下来的 线程栈一般是调用pthread_create时在文件映射区(共享区)mmap 创建在自己的线程管理块中的

#############################################################################

独立的上下文即有独立的PCB或者TCP

独立的栈即每个线程都有自己的栈要么是进程自己的要么是创建线程时mmap创建出来的

#############################################################################

对于子线程的stack,原则上是私有的 但是线程都在一个相同的地址空间中 实际上若是愿意 子线程之间可以互相访问

代码验证
#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>

int* p = nullptr;

void* routine(void* args)
{
    int a = 123;

    p = &a;

    while(true) {sleep(1);}
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, routine, nullptr);
    sleep(3);
    std::cout << *p << std::endl;

    pthread_join(tid, nullptr);

    return 0;
}

从这段代码中知道 其实线程之间也是可以互相访问的 

注意 主线程*p时 一定要在新线程改变p指向之后 否则解引用空指针 出现段错误


五、线程封装

#include <pthread.h>
#include <functional>
#include <iostream>
#include <cstdio>
#include <cstring>

namespace threadspace
{
    static uint32_t number = 1;
    using func_t = std::function<void()>;
    class Thread
    {
    private:
        void enableDetach()
        {
            std::cout << "线程被分离" << std::endl;
            _isdetach = true;
        }
        void enableRunning()
        {
            _isrunning = true;
        }
        // 新线程的入口函数应该是我们自己传的 保证创建的线程的入口函数不同 若是固定写在类内就是一个模板了
        // 所以要让类拿到传的函数 可以使用function将函数设置为一个属性
        // 但是在routine里面没有this指针拿不到这个属性 因为static保证routine参数只有一个不会出错
        // 所以在创建线程时 将this指针作为参数传到routine这样就能执行我们自己的入口函数了
        static void* routine(void *args)
        {
            // 这里不写成const void* 是因为void*无法解引用 因为不知道解引用之后是什么类型也就找不到成员属性
            Thread *const self = static_cast<Thread *const>(args);
            // 更仿真来说 新线程真正运行在routine里面 所以将这三行加到这里很说得通
            self->enableRunning(); // 设置标记位为运行状态
            if (self->_isdetach)
                self->Detach();
            self->_func();

            return nullptr;
        }

    public:
        Thread(func_t func)
            : _tid(0), _res(nullptr), _isdetach(false), _isrunning(false), _func(func)
        {
            _name = "thread-" + std::to_string(number);
        }
        void Detach()
        {
            // 这个接口要保证在创建前能detach 就是将_isdetach设置为true
            // 在创建之后detach 就是将新线程分离
            // 无论哪种都要将_isdetach设置为 true
            // 但是这都是在_isdetach 为 false的情况下
            if (_isdetach)
                return;
            if (_isrunning)
                pthread_detach(_tid);
            enableDetach();
        }
        bool Start()
        {
            if (_isrunning)
            {
                std::cout << "这个线程存在, 不能申请了" << std::endl;
                return false;
            }
            int n = pthread_create(&_tid, nullptr, routine, this);
            if (n != 0)
            {
                std::cerr << "线程创建失败 : " << std::strerror(n) << std::endl;
                return false;
            }

            std::cout << "线程创建成功" << std::endl;
            return true;
        }
        bool Stop()
        {
            if (!_isrunning)
            {
                std::cout << "线程没有运行, 无法取消" << std::endl;
                return false;
            }
            int n = pthread_cancel(_tid);
            if (n != 0)
            {
                std::cerr << "线程取消失败 : " << std::strerror(n) << std::endl;
                return false;
            }
            std::cout << "线程已经被取消" << std::endl;
            return true;
        }
        bool Join()
        {
            if (_isdetach)
            {
                std::cout << "这个线程是分离状态, 不用等待" << std::endl;
                return false;
            }
            int n = pthread_join(_tid, nullptr);
            if (n != 0)
            {
                std::cerr << "线程等待失败 : " << std::strerror(n) << std::endl;
                return false;
            }
            std::cout << "线程等待成功" << std::endl;
            return true;
        }
        ~Thread()
        {
        }

    private:
        pthread_t _tid;
        std::string _name;
        void *_res;
        bool _isdetach;
        bool _isrunning;
        func_t _func;
    };
};
#include "thread.hpp"
#include <unistd.h>
using namespace threadspace;

int main()
{
    Thread t1([](){
        while(true)
        {
            std::cout << "这是一个新线程" << std::endl;
            sleep(1);
        }
    });
    // 启动
    //t1.Detach();
    t1.Start();
    t1.Detach();
    
    sleep(5);

    t1.Stop();

    t1.Join();
    return 0;
}

分别不detach 创建前detach 创建后detach的运行结果


六、子问题

6.1、封装成线程模板可以传递任意参数

将线程封装为一个模板 这样就可以传递自定义类型或者内置类型了

#include <pthread.h>
#include <functional>
#include <iostream>
#include <cstdio>
#include <cstring>

namespace threadspace
{
    static uint32_t number = 1;

    template <typename T>
    class Thread
    {
    using func_t = std::function<void(T)>;

    private:
        void enableDetach()
        {
            std::cout << "线程被分离" << std::endl;
            _isdetach = true;
        }
        void enableRunning()
        {
            _isrunning = true;
        }

        static void *routine(void *args)
        {
            Thread<T> *const self = static_cast<Thread<T> *const>(args);
            self->enableRunning(); // 设置标记位为运行状态
            if (self->_isdetach)
                self->Detach();
            self->_func(self->_data);

            return nullptr;
        }

    public:
        Thread(func_t func, T data)
            : _tid(0), _res(nullptr), _isdetach(false), _isrunning(false), _func(func), _data(data)
        {
            _name = "thread-" + std::to_string(number);
        }
        void Detach()
        {
            if (_isdetach)
                return;
            if (_isrunning)
                pthread_detach(_tid);
            enableDetach();
        }
        bool Start()
        {
            if (_isrunning)
            {
                std::cout << "这个线程存在, 不能申请了" << std::endl;
                return false;
            }
            int n = pthread_create(&_tid, nullptr, routine, this);
            if (n != 0)
            {
                std::cerr << "线程创建失败 : " << std::strerror(n) << std::endl;
                return false;
            }

            std::cout << "线程创建成功" << std::endl;
            return true;
        }
        bool Stop()
        {
            if (!_isrunning)
            {
                std::cout << "线程没有运行, 无法取消" << std::endl;
                return false;
            }
            int n = pthread_cancel(_tid);
            if (n != 0)
            {
                std::cerr << "线程取消失败 : " << std::strerror(n) << std::endl;
                return false;
            }
            std::cout << "线程已经被取消" << std::endl;
            return true;
        }
        bool Join()
        {
            if (_isdetach)
            {
                std::cout << "这个线程是分离状态, 不用等待" << std::endl;
                return false;
            }
            int n = pthread_join(_tid, nullptr);
            if (n != 0)
            {
                std::cerr << "线程等待失败 : " << std::strerror(n) << std::endl;
                return false;
            }
            std::cout << "线程等待成功" << std::endl;
            return true;
        }
        ~Thread()
        {
        }

    private:
        pthread_t _tid;
        std::string _name;
        void *_res;
        bool _isdetach;
        bool _isrunning;
        func_t _func;
        T _data;
    };
};
#include "thread.hpp"
#include <unistd.h>
using namespace threadspace;


void Count(int cnt)
{
    while(cnt--)
    {
        std::cout << "这是一个新线程……" << std::endl;
        sleep(1);
    }
}
int main()
{
    int cnt = 10;

    Thread<int> t1(Count, cnt);

    t1.Start();

    t1.Stop();

    t1.Join();
    return 0;
}

还可以封装一个类传递给封装的线程

这个_data在新线程启动调用routine时回调func时传递给这个函数 类似于pthread_create中将arg传给routine

6.2、线程局部存储

先来看一段代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>

int num = 100;

void* routine1(void* args)
{
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "这是线程一, " << "num : " << num << std::endl;
        num += 33;
        sleep(1);
    }
    return nullptr;
}

void* routine2(void* args)
{
    int cnt = 5;
    while(cnt--)
    {
        std::cout << "这是线程二, " << "num : " << num << std::endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid1, tid2;
    // 线程1修改并且打印num
    pthread_create(&tid1, nullptr, routine1, nullptr);
    // 线程2只打印num
    pthread_create(&tid2, nullptr, routine2, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

从运行结果看出 这两个线程共享了num 这也是在预料之中的 因为它们属于一个进程地址空间

但是若是想要这两个线程拿到的是不同的num呢

此时引入线程局部存储的概念 

线程局部存储是一种让每个线程拥有独立的变量副本的机制,不同线程之间的变量互不干扰。这个独立副本存在于线程管理块中

6.3、利用线程局部存储获取线程的name

 int pthread_setname_np(pthread_t thread, const char *name);
       int pthread_getname_np(pthread_t thread,
                              char *name, size_t len);

在我们封装的线程调用routine中设置名字 之后在func中获取名字 打印即可

这里利用了线程局部存储 将_name放在了局部存储的地方

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值