Linux 线程(1)

1.线程概念

线程是进程内的最小执行单元,一个进程可以包含多个线程,所有线程共享进程的资源(内存、文件句柄等),但有自己独立的执行栈和程序计数器。


结合进程的核心区别可以这样理解:

进程是资源分配的基本单位
系统给进程分配内存、CPU 时间片等资源,进程像是一个 “独立的工作车间”。
线程是 CPU 调度的基本单位
线程是车间里的 “工人”,一个车间可以有多个工人,他们共享车间的工具和材料(进程资源),但各自干不同的活,且切换工人的开销远小于切换车间。

要是想使用线程,就得自己重新定义线程的结构体,相当于从头重新弄一套线程相关的实现;而 Linux 那边好像是直接用了原本就有的线程那一套机制,把它叫做轻量级进程

wiindows 系统就算重新弄了一套相关的实现

我们可以简单理解成

线程:是进程内的一个执行分支

如何理解我们以前的进程???

操作系统以进程为单位,给我们分配资源,我们当前的进程内部,只有一个线程(主线程)!


Linux 实现方案
在 Linux 中,线程在进程 “内部” 执行,线程在进程的地址空间内运行

任何执行流要执行,都要有资源!地址空间是进程的资源窗口

2.线程的性质

同一进程内的多个线程:共享内核空间的数据页表(且共享用户空间页表,因线程共享进程的页表根目录 PGD,内核空间映射全局统一);


不同进程的线程:逻辑上共享内核空间页表(所有进程的内核空间页表映射完全一致,系统启动时初始化一次,全局复用),但用户空间页表相互独立。

我们还是用以前虚拟地址的图片

相同进程不同线程不同进程不同线程      
内核空间页表相同相同
用户空间页表相同不同

假设我们创建了n个进程 

内核空间页表只有1份

用户空间页表有n份

无论一个进程里面有几个线程 一个进程里面只有一份独立的用户空间页表

和一份和其他进程共享的一份内核空间页表

线程切换比进程快 为什么???

线程共享进程的地址空间、文件等资源,切换时不用换页表、刷新缓存(这些是进程切换的大开销);
仅需保存 / 恢复 CPU 执行状态(比如寄存器、程序计数器),不用处理进程级的资源上下文。

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

3.虚拟地址到物理地址的转换方式

高 10 位(PDE 索引)= 楼栋号
小区太大,先按 “楼栋” 分组(页目录就是小区的「楼栋列表」,CR3 寄存器是小区大门的指引牌,告诉你楼栋列表放在小区哪个角落)。

高 10 位就是你快递上的 “楼栋号”,用它查楼栋列表,就能找到目标快递所在的楼栋(对应 “找到目标页表的物理地址”)。


中 10 位(PTE 索引)= 楼层号
找到楼栋后,还要找具体楼层(页表就是这栋楼的「楼层列表」)。
中 10 位是 “楼层号”,用它查楼层列表,就能找到目标快递所在的那一层(对应 “找到目标物理页帧的地址”)。

低 12 位(页内偏移)= 房间号
每一层的户型大小都一样(都是 4KB,因为 2^12=4096 字节),房间号范围固定(0-4095)。
低 12 位直接就是 “房间号”,不用再查列表,直接顺着楼层找到对应的房间(对应 “物理页帧内的具体字节位置”)。

如果不拆分,直接用 32 位虚拟地址对应物理地址,会有两个大问题:
浪费内存:


32 位系统最大虚拟地址是 4GB,按 4KB 一页算,需要 100 万个 “页表项”(4GB/4KB=1024*1024),每个页表项占 4 字节,一个进程的页表就占 4MB。如果有 1000 个进程,光页表就占 4GB,内存直接炸了!
拆分后(10+10+12):
页目录只需要 1024 个项(10 位),占 4KB(1024*4 字节);
每个页表也只需要 1024 个项,占 4KB;
只有进程用到的页表才会加载到内存,不用一次性加载所有页表,比如一个进程只用到 10 个页,总页表占用才 4KB(页目录)+10*4KB(页表)=44KB,比 4MB 省了近 100 倍!
查找更快:
拆分后是 “三级索引”(页目录→页表→页内偏移),每级都是固定长度的索引(10 位、10 位、12 位),CPU 的 MMU(内存管理单元)能像 “查字典按部首→页码→行数” 一样,硬件级快速计算地址,比线性查找快得多。

4.线程接口

线程函数的接口统一的一个头文件

#include <pthread.h>

1.pthread_create

作用是启动一个新的执行流(线程)。

int pthread_create(
    pthread_t *thread,        // 输出:新线程的ID(类似进程PID)
    const pthread_attr_t *attr, // 线程属性:NULL表示用默认属性
    void *(*start_routine)(void *), // 线程要执行的函数(函数指针)
    void *arg                 // 传递给start_routine的参数
);

thread:传入一个pthread_t类型的指针,函数会把新线程的 ID 写入这个指针指向的变量。
attr:线程属性(如栈大小、调度策略),一般填NULL用默认属性。
start_routine:线程要执行的函数,必须是void* (*)(void*)类型(入参是void*,返回值是void*)。
arg:传递给start_routine的参数,若要传多个参数,需封装成结构体指针。
返回值:成功返回0;失败返回错误码(不是-1,需用strerror()查看错误信息)。

2. pthread_join

int pthread_join(
    pthread_t thread,  // 要等待的子线程ID(由pthread_create生成)
    void **retval      // 输出:存储子线程的返回值;不需要则填NULL
);

thread:目标子线程的 ID(pthread_t类型,由pthread_create写入)。
retval:二级指针,用于接收子线程return的void*返回值;若无需返回值,填NULL。
返回值:成功返回0;失败返回错误码(非-1,可用strerror()查看错误信息)。

阻塞等待:调用pthread_join的线程(通常是主线程)会阻塞,直到目标子线程执行完毕。
回收资源:子线程结束后,若不调用pthread_join,其资源(如线程控制块 TCB、栈)不会自动释放,会成为 “僵尸线程”,导致资源泄露。
获取返回值:可以拿到子线程的执行结果(子线程通过return返回的void*数据)。

3.pthread_self()

获取当前线程 ID

pthread_t pthread_self(void);

返回值
当前线程的 pthread_t 类型 ID。

4.pthread_exit()

void pthread_exit(void *retval);

前面我们学过的exit和_exit是进程的主动退出方式

pthread_exit()是线程的主动退出方式

立即终止调用线程,将 retval 作为线程的退出状态(可被 pthread_join 获取)。

retval:线程退出的返回值指针(不能指向线程栈上的局部变量,因为线程退出后栈会被销毁)。

返回值
无(线程已终止,不会返回)。

线程函数中可显式调用 pthread_exit 退出,也可通过 return 返回(效果等价,return 的返回值会被当作 pthread_exit 的 retval)。


主线程特殊行为:主线程调用 pthread_exit 后,主线程终止,但进程不会退出,其他子线程会继续运行(区别于 exit(),exit() 会终止整个进程)。

!!!注意!!!

主线程从main函数return时,其他线程会被终止,但这一现象的本质并非 “主线程 return 直接终止子线程”,而是主线程 return 触发了进程退出,进程退出会导致所有线程被内核终止。

普通子线程的return    该线程结束,返回退出状态    继续运行    不受影响,正常运行

无论是子线程还是主线程调用exit的结果都是一样的 进程退出

5.pthread_detach()

int pthread_detach(pthread_t thread);

设置线程为分离状态

将指定线程设置为分离状态(detached),线程退出后,其占用的系统资源会被内核自动回收,无需调用 pthread_join 等待。

thread:需要设置为分离状态的线程 ID。
返回值
成功:返回 0;
失败:返回非 0 的错误码(如 EINVAL:线程无效或已分离;ESRCH:线程不存在)。

主线程中调用:pthread_detach(tid)(tid 是创建的线程 ID);
线程内部调用:pthread_detach(pthread_self())(推荐,线程自身控制分离状态);

线程一旦被分离,无法再被 pthread_join 等待,调用 pthread_join 会返回 EINVAL 错误。
不能对已被 pthread_join 的线程调用 pthread_detach(会失败)。
若线程未被分离且未被 pthread_join,退出后会变成僵尸线程,占用系统资源直到进程退出。

5.pthread_cancel()

int pthread_cancel(pthread_t thread);

发送线程取消请求

向指定线程发送取消请求,请求线程终止运行。线程是否响应、何时响应取决于其取消状态和取消类型。
参数
thread:需要取消的线程 ID。
返回值
成功:返回 0;
失败:返回非 0 的错误码(如 ESRCH:线程不存在)。

pthread_cancel() 仅发送请求,并非立即终止线程。
线程响应取消后,会返回 PTHREAD_CANCELED(宏定义,值为 (void*)-1),可被 pthread_join 获取。

我们可以写三个代码用来测试这些函数

#include <stdio.h>
#include <pthread.h>

void *thread_work(void *arg) {
    printf("【子线程】启动\n");
    
    unsigned long tid = (unsigned long)pthread_self();
    printf("【pthread_self()】执行成功,子线程自身ID:%lu\n", tid);
    
    printf("【pthread_exit()】即将执行,执行后子线程终止,后续代码不会运行\n");
    pthread_exit(NULL);
    printf("【pthread_exit()】未生效(此行不该打印)\n");
}

int main() {
    pthread_t tid;
    
    printf("【主线程】pthread_create()即将创建子线程\n");
    pthread_create(&tid, NULL, thread_work, NULL);
    printf("【pthread_create()】执行成功,创建的子线程ID:%lu\n", (unsigned long)tid);
    
    printf("【主线程】pthread_join()即将等待子线程结束\n");
    pthread_join(tid, NULL);
    printf("【pthread_join()】执行成功,子线程已结束\n");
    
    return 0;
}

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

// 线程工作函数(极简版)
void* thread_func(void* arg) {
    char* name = (char*)arg;
    for (int i = 1; i <= 3; i++) { // 循环3次,简化次数
        printf("线程[%s]:第%d次执行\n", name, i);
        sleep(1); // 取消点:响应cancel
    }
    printf("线程[%s]:执行完毕\n", name); // 被取消则不执行
    return NULL;
}

int main() {
    pthread_t t1, t2;

    // 创建线程t1(不取消)、t2(将取消)
    pthread_create(&t1, NULL, thread_func, "t1(不取消)");
    pthread_create(&t2, NULL, thread_func, "t2(将取消)");

    sleep(2); // 让t2执行2次后取消
    pthread_cancel(t2); // 发送取消请求

    // 等待线程结束
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

// 线程工作函数(极简版)
void* thread_func(void* arg) {
    printf("线程[%s]执行\n", (char*)arg);
    sleep(1); // 模拟工作
    printf("线程[%s]结束\n", (char*)arg);
    return NULL;
}

int main() {
    pthread_t t1, t2;
    int ret;

    // 创建非分离线程t1、分离线程t2
    pthread_create(&t1, NULL, thread_func, "t1(未分离)");
    pthread_create(&t2, NULL, thread_func, "t2(已分离)");
    pthread_detach(t2); // 设置分离

    // 尝试join非分离线程t1(成功)
    ret = pthread_join(t1, NULL);
    printf("join t1:%s\n", ret ? strerror(ret) : "成功");

    // 尝试join分离线程t2(失败)
    ret = pthread_join(t2, NULL);
    printf("join t2:%s(预期失败)\n", ret ? strerror(ret) : "成功");

    sleep(1);
    return 0;
}

5.修饰符__thread

其核心作用是让被修饰的变量成为每个线程的私有副本,而非进程级的共享变量。

___thread只能对内置类型使用 不能对自定义类型使用

1.__thread定义在线程函数内时,主线程是否有副本,完全取决于主线程是否执行了这个定义了__thread变量的函数—— 只有执行了函数,才会触发副本的分配;未执行则无副本。

2.__thread定义在全局时 主线程必定拥有副本,这是进程启动的默认行为,与是否使用变量无关;
所有子线程创建时会自动获得独立副本,初始化值为编译期常量;

#include <stdio.h>
#include <pthread.h>

// 普通全局变量(共享)
int s = 0;
// __thread变量(线程私有)
__thread int t = 0;

// 线程函数:仅做一次自增+打印(精简核心逻辑)
void* f(void* arg) {
    int id = *(int*)arg;
    s++, t++; // 共享变量自增,私有变量自增
    printf("线程%d: s=%d, t=%d\n", id, s, t);
    return NULL;
}

int main() {
    pthread_t a, b;
    int id1=1, id2=2;
    pthread_create(&a, NULL, f, &id1);
    pthread_create(&b, NULL, f, &id2);
    pthread_join(a, NULL);
    pthread_join(b, NULL);
    printf("主线程: s=%d, t=%d\n", s, t); // 主线程的t是独立副本
    return 0;
}

6.线程管理

我们学语言的时候知道 临时变量是有生命周期的 是位于栈上的

对于线程同样适用

主线程和子线程一样有它们的栈区 但是子线程和主线程栈的位置在进程地址空间的不同地方

主线程的栈区位于进程地址空间中的用户地址空间中的栈区

而线程为了方便统一管理  被封装到一个库里面 运行的时候会被加载到内存的共享区

子进程的栈区位于进程地址空间中的用户地址空间中的mmap即共享区(暂时先这么理解)

这张图右侧的struct pthread对应的是用户态的线程控制块(TCB)主线程的tcb也在其中

但是主线程的栈是特殊的(位于进程默认栈区)—— 主线程的 TCB(struct pthread)虽然在 mmap 区,但它记录的主线程栈地址是「进程默认栈区」,

那么看到前面代码的运行结果

你不好奇为什么线程的tid这么大呢

线程在里面叫轻量化进程 进程有pid 线程同样也有tid

每一个线程的库级别的 tcb 的起始地址,叫做线程的 tid!!

线程在里面叫轻量化进程 进程有pcb 线程同样也有tcb

维度主线程 ID子线程 ID
用户态(pthread_t)类型为pthread_t,指向主线程的 TCB 地址类型为pthread_t,指向子线程的 TCB 地址
内核态(LWP)pid_t(整数类型,本质是内核轻量级进程编号)等于当前进程的 PID,内核全局唯一pid_t(整数类型,本质是内核轻量级进程编号)内核分配的独立pid_t类型整数(≠进程 PID),内核全局唯一

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值