文章目录
- POSIX线程库
- 创建线程
- 线程ID及进程地址空间布局
- 线程等待
- pthread_join
- 线程终止
- pthread_exit函数
- pthread_cancel函数
- 线程分离
- 理解pthread库
POSIX线程库
POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。
实现POSIX 线程标准的库常被称作Pthreads,一般用于Unix-likePOSIX 系统,如Linux、Solaris。但是Microsoft Windows上的实现也存在,例如直接使用Windows API实现的第三方库pthreads-w32;而利用Windows的SFU/SUA子系统,则可以使用微软提供的一部分原生POSIX API。
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的要使用这些函数库,要通过引入头文<pthread.h>链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
在上篇文章提到,Linux的内核并没有真正实现线程的相关接口和相关的结构,而是使用进程的PCB模拟实现的,所以需要通过操作进程的相关接口来完成线程的控制,例如线程创建,线程等待,线程终止,线程分离等,所以最初的系统工程师,就将这些接口封装起来制作了pthread库,来在用户层实现线程的相关操作。
创建线程
功能:创建一个新的线程
原型
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;失败返回错误码
第一个参数是一个输出型参数,可以得到创建的线程的id,第二个参数可以设置线程的属性,我们通常传入NULL来设置默认,让编译器来处理,第三个参数是一个函数指针,是传给线程启动后要执行的函数,第四个参数是要传给启动函数的参数。
返回值:成功返回0,失败返回错误码
下边通过一段程序来验证一下:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM 5
void* pthread_run(void* args)
{
while(1){
int num = *(int*)args;
printf("我是新线程[%d], 我的线程ID是: %lu\n", num,pthread_self());
sleep(2);
break;
}
}
int main()
{
pthread_t tid[NUM];
for(int i=0; i<NUM; ++i)
{
pthread_create(&tid[i],NULL,pthread_run,(void*)&i);
sleep(1);
}
while(1)
{
printf("我是主线程, 我的thread ID: %lu\n", pthread_self());
printf("#########################begin########################\n");
for(int i = 0; i < NUM; ++i)
{
printf("我创建的线程[%d]是: %lu\n", i, tid[i]);
}
printf("#########################end##########################\n");
sleep(1);
}
return 0;
}
通过pthread_create接口,就可以创建了一个线程。
线程ID及进程地址空间布局
通过上边程序的验证,我们看到了每创建一个线程,就会有一个线程id,但是这个id代表什么呢?其实他代表一个地址,我们再通过16进制打印一下这个地址:
那么这个地址到底代表什么呢?我们一起来探究一下:
在进程运行起来之后,使用ps -aL指令可以查看轻量级进程,也就是LWP,而且我们发现第一个第一个线程的PID和LWP是相同的,这个就是主线程,而我们发现此处的轻量级进程ID和刚才获得的线程ID不一样,其实LWP才是内核中的线程ID,而刚才获得的只是用户级的ID。
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的,所以通过pthread_create接口获得的用户级线程是动态库中属于某一个线程结构体在进程地址空间中存放的地址。
对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
线程等待
和进程等待类似,如果不对线程进行等待,也可能会有僵尸进程的情况,已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,创建新的线程不会复用刚才退出线程的地址空间。
当一个线程退出时,也有三种情况:
代码跑完,结果不对。
代码跑完,结果对。
代码没有跑完,就异常终止了。
pthread_join
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
第二个参数是一个二级指针,因为线程的启动函数的返回值是一个void*类型的变量,所以为了拿到这个返回值,我们使用二级指针来接收。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM 1
void* pthread_run(void* args)
{
while(1)
{
int num = *(int*)args;
printf("我是新线程[%d], 我的线程ID是: 0x%x\n", num,pthread_self());
sleep(5);
pthread_exit((void*)123);
}
}
int main()
{
pthread_t tid[NUM];
for(int i=0; i<NUM; ++i)
{
pthread_create(&tid[i],NULL,pthread_run,(void*)&i);
sleep(1);
}
void *status = NULL;
int ret = 0;
for(int i = 0; i < NUM; i++)
{
ret = pthread_join(tid[i], &status);
}
printf("ret: %d, status: %d\n", ret, (int)status);
return 0;
}
此时的返回值为0,所以成功返回了,并且status退出码为123,就是我们之前设置的开启函数的退出码。
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr可以得到退出码信息,不关心就可以传入NULL,不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
使用pthread_exit函数可以终止自己,所以可以使用我们创建的线程来终止自己。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM 1
void* pthread_run(void* args)
{
while(1)
{
int num = *(int*)args;
printf("我是新线程[%d], 我的线程ID是: 0x%x\n", num,pthread_self());
sleep(5);
pthread_exit(NULL);
}
}
int main()
{
pthread_t tid[NUM];
for(int i=0; i<NUM; ++i)
{
pthread_create(&tid[i],NULL,pthread_run,(void*)&i);
sleep(1);
}
while(1)
{
printf("我是主线程, 我的thread ID: 0x%x\n", pthread_self());
printf("#########################begin########################\n");
for(int i = 0; i < NUM; ++i)
{
printf("我创建的线程[%d]是: 0x%x\n", i, tid[i]);
}
printf("#########################end########################\n");
sleep(1);
}
return 0;
}
pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM 1
void* pthread_run(void* args)
{
while(1)
{
int num = *(int*)args;
printf("我是新线程[%d], 我的线程ID是: 0x%x\n", num,pthread_self());
sleep(2);
}
}
int main()
{
pthread_t tid[NUM];
for(int i=0; i<NUM; ++i)
{
pthread_create(&tid[i],NULL,pthread_run,(void*)&i);
sleep(1);
}
printf("wait sub thread....\n");
sleep(1);
printf("cancel sub thread ...\n");
pthread_cancel(tid[0]);
void *status = NULL;
int ret = 0;
for(int i = 0; i < NUM; i++)
{
ret = pthread_join(tid[i], &status);
}
printf("ret: %d, status: %d\n", ret, (int)status);
return 0;
}
线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。所以我们可以分离线程来让线程退出时,自动释放线程资源。
函数:pthread_detach
detach:分开,脱离
头文件:#include <pthread.h>
函数原型:
int pthread_detach(pthread_t thread);
参数:
thread:被分离线程的ID
返回值:
线程分离成功返回0,失败返回错误码
可以是线程组中其他线程来分离某一线程
int pthread_detach(pthread_t thread);
也可以是自身线程来分离线程
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的,所以线程分离之后,就不能使用主线程来等待了
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#define NUM 1
void* pthread_run(void* args)
{
pthread_detach(pthread_self());
while(1){
int num = *(int*)args;
printf("我是新线程[%d], 我的线程ID是: 0x%x\n", num,pthread_self());
sleep(2);
}
}
int main()
{
pthread_t tid[NUM];
for(int i=0; i<NUM; ++i)
{
pthread_create(&tid[i],NULL,pthread_run,(void*)&i);
sleep(1);
}
void *status = NULL;
int ret = 0;
for(int i = 0; i < NUM; i++)
{
ret = pthread_join(tid[i], &status);
}
printf("ret: %d, status: %d\n", ret, (int)status);
return 0;
}
此时等待的返回值不为0,所以此时等待失败。
理解pthread库
任何语言,要在Linux上使用多线程,底层必须封装pthread库,其实pthread库是在用户级别的,而库中对应着每个线程的相关属性以及他们的栈,用户级得到的id是在对应某一线程的动态库在虚拟地址空间中的地址,而内核中的TWP才是内核中该线程的id,这个id一定存储在该线程动态库的结构体中,他是线程的一个属性,而线程的动态库被加载到进程地址空间的共享区中。