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

【Linux笔记】——Linux线程控制创建、终止与等待|动态库与内核联动

🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹:【Linux笔记】——Linux线程理解与分页存储的奥秘
🔖流水不争,争的是滔滔不息


  • 一、线程库
    • POSIX线程库
    • pthread线程库
  • 二、创建线程
  • 三、线程终止
  • 四、线程等待
  • 五、线程分离
  • 六、线程ID及进程地址空间布局
    • **动态库与内核是怎么联动的**

一、线程库

POSIX线程库

POSIX线程库(POSIX Threads,简称Pthreads)是一个用于多线程编程的标准API,广泛应用于Unix-like操作系统中。它提供了一套丰富的函数,用于创建、管理和同步线程,使得开发者能够编写高效的多线程程序。

POSIX线程库的核心功能

  • 线程创建与管理:通过pthread_create函数创建新线程,pthread_exit函数终止线程,pthread_join函数等待线程结束。
  • 线程同步:提供了互斥锁(pthread_mutex_t)、条件变量(pthread_cond_t)等机制,用于线程间的同步。
  • 线程属性设置:通过pthread_attr_t结构体,可以设置线程的栈大小、调度策略等属性。

POSIX线程库的核心优势

  • 高并发服务器:通过多线程处理并发请求,提升服务器的响应速度。
  • 并行计算:将计算任务分解为多个线程,充分利用多核处理器的计算能力。
  • 实时系统:通过线程优先级设置,满足实时系统的响应时间要求。

pthread线程库

pthread 是 POSIX 线程库在 Linux 上的实现。在 Linux 系统中,glibc 中包含了 POSIX 线程标准的一个具体实现,我们就称之为 pthread 线程库。所以在 Linux 中,我们写的多线程代码(pthread_create、pthread_mutex_lock 等)其实是调用 pthread 实现,它遵循 POSIX 线程规范。


POSIX Threads: 标准接口规范(由 IEEE 定义)
pthread: Linux 中对该标准的实现(由 glibc 提供)

二、创建线程

//功能:创建⼀个新的线程
//原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

参数:
thread: 返回线程ID
attr: 设置线程的属性,attr为NULL表示使用默认属性
start_routine: 是个函数地址,线程启动后要执行的函数
arg: 传给线程启动函数的参数

返回值:成功返回0;失败返回错误码


传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。

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

pthread_t 是 POSIX 线程库中定义的线程标识符类型,你可以把它理解为线程的“用户态句柄”,用来标识和操作一个线程。它的具体实现是平台相关的,比如在 Linux 上可能是一个无符号长整型,也可能是指针或结构体,但这些你不用操心,直接当成一个不透明的标识符用就行了。
上面代码中pthread_t tid 是 POSIX 线程库维护的“用户态线程标识”,用于在线程创建、管理中代表线程本身;它不是内核的线程 ID,也不是进程 PID,但足够我们在线程编程中使用。

如下图,用ps-aL命令来查看线程信息
在这里插入图片描述

在 Linux 中,每个线程都是一个轻量级进程(LWP)拥有自己独立的线程 ID(TID)共享同一个进程 ID(PID)以及虚拟地址空间。主线程的栈在虚拟地址空间的栈上,而其他线程的栈在是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
主线程和其他线程本质上地位相同,只是主线程创建了其他线程。如果任意线程崩溃,比如访问了非法地址,整个进程都会被终止。
在这里插入图片描述
发现上图中消息混在一起。本质上就是因为没有互斥同步机制,导致多个线程同时访问共享资源(比如标准输出)时发生了竞争。这个后面聊。

三、线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. ⼀个线程可以调用pthread_ cancel终止同⼀进程中的另⼀个线程。

pthread_ exit函数

void pthread_exit(void *value_ptr);

功能:线程终止
参数:value_ptr:value_ptr不要指向⼀个局部变量。
返回值:无返回值,跟进程⼀样,线程结束的时候无法返回到它的调用者(自身)

示例:线程自己用 pthread_exit 退出

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>void* thread_func(void* arg) {printf("子线程开始执行\n");sleep(2);printf("子线程准备退出\n");pthread_exit(NULL);  // 显式终止自己
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);pthread_join(tid, NULL);printf("主线程结束\n");return 0;
}

上述代码:子线程运行完后显式调用 pthread_exit。如果不用 pthread_exit,return 也是可以的。主线程使用 pthread_join 等待子线程结束。


pthread_ cancel函数

int pthread_cancel(pthread_t thread);

功能:取消⼀个执行中的线程
参数:thread:线程ID
返回值:成功返回0;失败返回错误码

示例:主线程使用 pthread_cancel 取消子线程

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>void* thread_func(void* arg) {printf("子线程运行中...\n");while (1) {printf("子线程工作中...\n");sleep(1);}return NULL;
}int main() {pthread_t tid;pthread_create(&tid, NULL, thread_func, NULL);sleep(3);  // 等子线程跑一会printf("主线程准备取消子线程\n");pthread_cancel(tid);  // 强制终止子线程pthread_join(tid, NULL);printf("主线程结束\n");return 0;
}

上述代码:子线程一直运行,主线程在 3 秒后调用 pthread_cancel 终止它。如果子线程没有设置取消点(比如 sleep() 就是一个),可能不会立刻退出。实际项目中推荐用取消标志 + pthread_testcancel实现可控终止。

四、线程等待

线程等待是指一个线程在执行过程中暂停运行,直到某个条件满足或某个事件发生后再继续执行。线程等待通常用于多线程编程中,以协调多个线程的执行顺序或资源共享。

为什么需要线程等待?

  1. 确保线程执行完毕再退出主程序
  2. 获取子线程的返回值如果你想要拿到子线程的计算结果,必须 pthread_join 才能获取到返回值。
  3. 防止资源泄漏:线程句柄的回收pthread_join 会自动释放线程资源(包括线程栈、状态等)。不等待就会有资源泄漏风险,尤其是你线程多时。
  4. 控制并发、保证先后顺序。比如你想要多个线程先执行完任务,然后主线程再处理汇总,就必须等待它们。
int pthread_join(pthread_t thread, void **value_ptr);

参数:thread:线程ID,value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码


其实可以这样理解线程等待就像是在“回收子线程”

  1. 资源回收(就像回收尸体 🧟)
    每个线程结束后,操作系统会保留一块内核资源来保存它的返回状态,等着别的线程来拿。如果你不 pthread_join,那这块资源就留在那了,叫做“僵尸线程”(zombie thread)——虽然线程执行完了,但操作系统不能释放它的资源。
    类似于进程的“僵尸进程”。

  2. 流程控制(就像等人干完活再收摊)
    你可能希望子线程完成某项工作后再继续执行下一步(比如汇总数据、保存结果等),那你就需要 pthread_join 等它。否则可能主线程提前结束,导致子线程还没来得及干活就整个程序结束了

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>void* thread_func(void* arg) {int num = *(int*)arg;printf("子线程:我是线程 %d,线程ID: %lu\n", num, pthread_self());sleep(1); // 模拟工作负载return NULL;
}int main() {pthread_t tid;int thread_num = 1;printf("主线程:创建一个子线程\n");if (pthread_create(&tid, NULL, thread_func, &thread_num) != 0) {perror("pthread_create 出错");return 1;}printf("主线程:等待子线程结束\n");if (pthread_join(tid, NULL) != 0) {perror("pthread_join 出错");return 1;}printf("主线程:子线程已结束,回收完毕\n");return 0;
}

pthread_t tid;声明一个线程标识符,类似于线程的“身份证”。pthread_create(…)创建一个线程,执行 thread_func,并传入参数。pthread_self()获取当前线程的ID(用于打印和调试)。pthread_join(…)
主线程等待子线程结束,并进行资源回收。

五、线程分离

线程分离指的是一个线程在创建之后,不需要其他线程(通常是主线程)去pthread_join()等待它结束并回收资源,而是线程自己结束之后,自动释放自己的资源。

int pthread_detach(pthread_t thread);

如何理解线程分离?
默认情况下,线程是“可连接的(joinable)”,也就是说:你必须显式地 pthread_join() 去等它结束。不回收它的资源,它就会变成“僵尸线程”,内存资源不释放。线程分离的本质是告诉系统:“这个线程死了就让它自己埋了,别来找我(主线程)。”

注意:
一旦线程被分离,就不能再用 pthread_join() 来等待它,否则程序会出错。
分离线程常用于后台任务、服务线程等,不关心返回值和状态的情况。


在线程创建后分离

pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_detach(tid);  // 让这个线程变成分离状态

在创建时直接设置分离属性

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);pthread_t tid;
pthread_create(&tid, &attr, thread_func, NULL);
pthread_attr_destroy(&attr); // 用完释放属性对象

在现实中线程分离的例子
在服务器中给每个服务器创建一个处理线程,然后立刻将其设置为分离线程,因为服务器不关心这些线程何时结束,只要它们把任务处理完就行。

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>void* handle_client(void* arg) {int client_id = *(int*)arg;free(arg); // 动态分配的数据,及时释放printf("开始处理客户端 %d\n", client_id);sleep(2);  // 模拟处理过程printf("客户端 %d 处理完毕\n", client_id);return NULL;
}int main() {for (int i = 0; i < 5; ++i) {pthread_t tid;int* client_id = malloc(sizeof(int));*client_id = i + 1;pthread_create(&tid, NULL, handle_client, client_id);pthread_detach(tid);  // 让线程自己收尾,不用 joinprintf("客户端 %d 已分配线程\n", *client_id);}sleep(5); // 等子线程处理完毕printf("服务器主线程结束\n");return 0;
}

六、线程ID及进程地址空间布局

在这里插入图片描述
pthread库文件是在磁盘中的,pthread库文件会加载到内存中,然后通过库映射映射到动态库映射区(库的映射的大致流程)。通过之前文件系统的学习,进程自己的代码区可以访问共享区的函数和数据,代码可以访问pthread库内部的函数和代码。

线程的抽象概念是在用户态的库(用户态的 pthread 库(比如 libpthread.so)是以共享库的形式映射到每个使用它的进程的地址空间中)中维护的。管理线程需要先描述在组织。

用户态(库,比如 pthread) 维护线程的“概念”:线程控制块(TCB)、pthread_t、创建关系、回收逻辑、join 状态等

在这里插入图片描述
调用pthread库中的函数,会在动态库里分配一块内存作为这个线程的用户态控制块(TCB),设置好栈、启动函数等信息。调用内核的clone系统调用来创建真正的内核线程。在动态库中,去执行你要执行的函数然后在这块内存的响应位置保存返回的结果(void* ret)。pthread_t tid保存线程的信息(可能就是TCB的地址)。比方说我们要进程线程等待,pthread_join(tid,&ret),tid就让库知道等待哪个库了,ret就是执行的结果,这也从侧面说明了我们为什么要等待,资源都在动态库中存储,不等待就内存泄露了。

ChatGPT对上述我的总结进一步总结与优化: 当我们调用 pthread_create 时,其实是进入了 libpthread 动态库内部:库中会为每个线程分配一块用户态内存作为线程控制块(TCB),准备栈空间、设置好要执行的函数与参数,然后再调用 clone()系统调用创建真正的内核线程。 线程函数执行完的返回值也会被写入 TCB 的相应位置。pthread_t通常就是线程控制块的一个句柄或地址。后续调用 pthread_join(tid, &ret),库就知道要等待哪个线程,并能从它的 TCB中取出执行结果 ret。这也说明了为什么要等待线程退出:如果不回收,TCB 和栈空间就泄露在用户态了。

动态库与内核是怎么联动的

调用 pthread_create() 后,库里先准备一切(TCB、栈、函数等),再用 clone() 让内核真正创建线程,线程运行后返回的结果也存在库里,pthread_join() 是为了拿结果和清理资源。

用户态(pthread 库)做的事:
分配一块内存作为 TCB(线程控制块)记录线程的启动函数、参数、返回值、栈地址等。这块内存的地址可能就是 pthread_t tid。准备线程栈,没指定的话就自动分配一块默认大小的栈。设置好函数调用信息,把你要跑的函数 thread_func(arg) 等等全都准备好。调用系统调用 clone(),把准备好的信息告诉内核,要求内核创建一个线程来执行。
内核做的事(clone()):
创建线程的 task_struct(轻量进程)和主线程共享虚拟地址空间。使用你传的栈和函数。调度线程去跑你准备的函数

执行完之后:
线程跑完后,返回值会保存在 TCB 中。如果主线程调用 pthread_join(tid, &ret):就能拿到返回值;并且自动释放这块 TCB 和栈;不 join 就内存泄露。


线程重要的两点:
独立的上下文和独立的栈空间。
独立的上下文是线程有独立的PCB(内核)+TCP(用户层,动态库中的pthread库内部)。
独立的栈是每个线程都有自己的栈,要么是进程自己的要么是库中创建进程的时候动态库中申请出来的。

为什么要有独立的上下文和独立的栈空间?
没有独立的上下文,跑着线程A呢突然切成线程B,但是用的信息还是线程A的寄存器,直接就跑错逻辑了。
没有独立的栈,多个线程共用一个栈,那么函数调用局部变量不就全冲突了,整个程序就炸了。

每个线程都是 CPU 眼中的「独立执行单元」,它要有自己的“脑子”(上下文)和“工作台”(栈),才能被调度运行而不互相干扰。

http://www.xdnf.cn/news/502597.html

相关文章:

  • 软件测试的常用的面试题【带答案】
  • 【汇总】影视仓接口地址,影视仓最新配置接口【2025.5】
  • 常见图算法解析:TSP问题、最大团/独立集问题、图着色问题、哈密尔顿回路问题、顶点覆盖问题和最长路径问题
  • Ocean: Object-aware Anchor-free Tracking
  • 中级网络工程师知识点4
  • 【文本切割器】RecursiveCharacterTextSplitter参数设置优化指南
  • ORACLE RAC环境REDO日志量突然增加的分析
  • 【以及好久没上号的闲聊】Unity记录8.1-地图-重构与优化
  • SQL Server 常用函数
  • QT使用QXlsx读取excel表格中的图片
  • 【自然语言处理与大模型】大模型(LLM)基础知识④
  • 日语学习-日语知识点小记-构建基础-JLPT-N4阶段(23):受身形
  • mAP、AP50、AR50:目标检测中的核心评价指标解析
  • 开源项目实战学习之YOLO11:12.2 ultralytics-models-sam-decoders.py源码分析
  • Vue百日学习计划Day19-20天详细计划-Gemini版
  • 密文搜索-map容器+substr
  • javaDoc
  • 电子电器架构 --- 整车造车阶段四个重要节点
  • Java卡与SSE技术融合实现企业级安全实时通讯
  • 提示词写的好,也可以生成EXE
  • MySQL多条件查询深度解析
  • Qt做的应用程序无法彻底关闭的问题解析
  • MySQL 查询执行流程全解析
  • IPD推行成功的核心要素(二十二)IPD流程持续优化性地推出具备商业成功潜力的产品与解决方案
  • 使用HtmlAgilityPack采集墨迹天气中的天气数据
  • 9.DMA
  • 如果丝杆有轴向窜动应如何处理?
  • 西门子 Teamcenter13 Eclipse RCP 开发 1.3 工具栏 单选按钮
  • 使用tensorRT10部署低光照补偿模型
  • 六、绘制图片