【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库是在共享区的。所以除了主线程之外的其他线程的栈都在共享区。
主线程和其他线程本质上地位相同,只是主线程创建了其他线程。如果任意线程崩溃,比如访问了非法地址,整个进程都会被终止。
发现上图中消息混在一起。本质上就是因为没有互斥同步机制,导致多个线程同时访问共享资源(比如标准输出)时发生了竞争。这个后面聊。
三、线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- ⼀个线程可以调用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实现可控终止。
四、线程等待
线程等待是指一个线程在执行过程中暂停运行,直到某个条件满足或某个事件发生后再继续执行。线程等待通常用于多线程编程中,以协调多个线程的执行顺序或资源共享。
为什么需要线程等待?
- 确保线程执行完毕再退出主程序
- 获取子线程的返回值如果你想要拿到子线程的计算结果,必须 pthread_join 才能获取到返回值。
- 防止资源泄漏:线程句柄的回收pthread_join 会自动释放线程资源(包括线程栈、状态等)。不等待就会有资源泄漏风险,尤其是你线程多时。
- 控制并发、保证先后顺序。比如你想要多个线程先执行完任务,然后主线程再处理汇总,就必须等待它们。
int pthread_join(pthread_t thread, void **value_ptr);
参数:thread:线程ID,value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
其实可以这样理解线程等待就像是在“回收子线程”
-
资源回收(就像回收尸体 🧟)
每个线程结束后,操作系统会保留一块内核资源来保存它的返回状态,等着别的线程来拿。如果你不 pthread_join,那这块资源就留在那了,叫做“僵尸线程”(zombie thread)——虽然线程执行完了,但操作系统不能释放它的资源。
类似于进程的“僵尸进程”。 -
流程控制(就像等人干完活再收摊)
你可能希望子线程完成某项工作后再继续执行下一步(比如汇总数据、保存结果等),那你就需要 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 眼中的「独立执行单元」,它要有自己的“脑子”(上下文)和“工作台”(栈),才能被调度运行而不互相干扰。