本篇我将学习如何使用多线程。要使用多线程,因为Linux没有给一般用户直接提供操作线程的接口,我们使用的接口,都是系统工程师封装打包成原生线程库中的。那么就需要用到原生线程库。因此,需要引入-lpthread,即连接原生线程库。
原生线程库:#include <pthread.h> 自动化构建工具: mythread:mythread.c gcc -o $@ $^ -lpthread .PHONY:clean clean: rm -f mythread
创建线程
创建线程。
功能:创建一个新的线程 原型: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。即哪个线程调用了它,就能够获得自己的id。
函数原型:pthread_t pthread_self(void); 功能:获取一个线程id,即谁调用它,就获取谁的线程id 头文件:#include <pthread.h> 参数:无 返回值:成功返回这个id。这个函数总是成功的!
创建一个线程,代码如下:
#include <stdio.h> #include <pthread.h> #include <unistd.h> void *thread_run(void *args) { while(1) { //使用pthread_self()获取id printf("我是新线程[%s],我的线程ID是: %lu\n",(const char*)args,pthread_self()); sleep(1); } } int main() { pthread_t tid; //第一个参数为线程id,第二个为线程属性,设置为NULL默认值 //第三个参数是线程执行的方法,第四个是传给第三个参数的参数 pthread_create(&tid,NULL,thread_run,(void*)"new thread"); while(1) { printf("我是主线程,我创建的线程ID是: %lu,我的线程ID是: %lu\n",tid,pthread_self()); sleep(1); } return 0; }
结果如下,能看到两个线程的ID不一样,那就证明了单个进程中存在着两个线程。
创建多个线程,代码如下:
使用数组来存放线程id。注意此时thread_run()函数被重入了!
#include <stdio.h> #include <pthread.h> #include <unistd.h> void *thread_run(void *args) { while(1) { //使用pthread_self()获取id // printf("我是新线程[%s],我创建的线程ID是: %lu\n",(const char*)args,pthread_self()); sleep(3); } } int main() { //创建五个线程,保存在数组中 pthread_t tid[5]; //第一个参数为线程id,第二个为线程属性,设置为NULL默认值 //第三个参数是线程执行的方法,第四个是传给第三个参数的参数 int i = 0; for(i = 0;i<5;++i) { pthread_create(tid+1,NULL,thread_run,(void*)"new thread"); } while(1) { printf("我是主线程,我的thread ID:%lu\n",pthread_self()); printf("######################begin########################\n"); for(i = 0;i<5;++i) { printf("我是主线程,我创建的线程[%d]ID是: %lu,我的线程ID是: %lu\n",i,tid[i],pthread_self()); } printf("######################end######################\n"); sleep(1); } return 0; }
结果如下:
通过ps -aL查看当前线程。可以看到,PID和LWP相同的就是主线程,其它的都是新线程。LWP是线程id。
线程等待
一般而言,线程也是需要等待的,如果不等待,就可能会导致类似于"僵尸进程"的问题。
功能:等待线程结束 原型:int pthread_join(pthread_t thread, void** value_ptr); 参数: thread : 线程ID value_ptr : 它指向一个指针,后者指向线程的返回值 返回值:成功返回0;失败返回错误码
写一个简单的测试,主线程在等待,10秒后打印111.
#include <stdio.h> #include <pthread.h> #include <unistd.h> void *thread_run(void *args) { int num = *(int*)args; while(1) { //使用pthread_self()获取id printf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self()); sleep(10); break; } //随便返回一个值用于测试 return (void*)111; } #define NUM 1 int main() { pthread_t tid[NUM]; int i = 0; for(i = 0;i<NUM;++i) { pthread_create(tid+1,NULL,thread_run,(void*)&i); sleep(1); } //指针变量可以当某个数据的容器 void *status = NULL; //获得退出信息 pthread_join(tid[0],&status); printf("ret: %d\n",(int)status); return 0; }
线程只能一个个等。
线程终止
线程终止的方案有:
1.函数中的return。对于这个方案有两种情况:第一种情况是在main函数中的return,此时代表进程和主线程都退出了。第二种情况是其它函数中的return,代表该线程的退出。
2.使用函数pthread_exit().
功能:线程终止 原型:void pthread_exit(void* value_ptr); 参数: value_ptr : z指的是退出后的返回值,也就是return X。value_ptr不要指向一个局部变量。 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
#include <stdio.h> #include <pthread.h> #include <unistd.h> void *thread_run(void *args) { int num = *(int*)args; while(1) { //使用pthread_self()获取id printf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self()); sleep(2); break; } pthread_exit((void*)123); } #define NUM 1 int main() { pthread_t tid[NUM]; int i = 0; for(i = 0;i<NUM;++i) { pthread_create(tid+1,NULL,thread_run,(void*)&i); sleep(1); } //指针变量可以当某个数据的容器 void *status = NULL; //获得退出信息 pthread_join(tid[0],&status); printf("ret: %d\n",(int)status); return 0; }
如果使用exit(),那么会将进程和全部线程都终止掉。
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,函数退出代表函数栈帧被销毁,从而这个内存单元也被销毁了。
3.使用pthread_cancel函数取消目标线程。
功能:取消一个执行中的线程 原型:int pthread_cancel(pthread_t thread); 参数: thread : 线程ID 返回值:成功返回0;失败返回错误码,退出码为-1
#include <stdio.h> #include <pthread.h> #include <unistd.h> void *thread_run(void *args) { int num = *(int*)args; while(1) { //使用pthread_self()获取id printf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self()); sleep(2); //break; } //pthread_exit((void*)123); //随便返回一个值用于测试 //return (void*)111; } #define NUM 1 int main() { pthread_t tid[NUM]; int i = 0; for(i = 0;i<NUM;++i) { pthread_create(tid+1,NULL,thread_run,(void*)&i); sleep(1); } printf("wait sub thread...\n"); //等5秒钟 sleep(5); printf("cancel sub thread...\n"); //取消线程 pthread_cancel(tid[0]); //指针变量可以当某个数据的容器 void *status = NULL; //获得退出信息 pthread_join(tid[0],&status); printf("ret: %d\n",(int)status); return 0; }
当一个新线程被取消后,退出码为-1,即PTHREAD_ CANCELED。
当把主线程取消,但新线程没有被取消,此时新线程依旧在运行着,并且主线程会进入"僵尸状态"(说明:线程没有僵尸状态这个东东,是有类似僵尸进程的问题)。因此我们一般不能用新线程去取消主线程。
线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
线程分离后,不需要被join终止,只需运行结束后会自动释放Z。分离后的线程相对于是同一屋檐下的陌生人,即这个线程在跟同一个进程内的线程毫无关系了,此时一定不能对其join,因为会失败。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
线程分离一般的应用场景是主线程不退出,新线程处理完任务后退出。
功能:分离线程 原型:int pthread_detach(pthread_t thread); 参数: thread : 线程ID 返回值:成功返回0;
#include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> void *thread_run(void *args) { //分离 pthread_detach(pthread_self()); int num = *(int*)args; while(1) { //使用pthread_self()获取id printf("我是新线程[%d],我创建的线程ID是: %lu\n",num,pthread_self()); sleep(2); break; } //随便返回一个值用于测试 return (void*)111; } #define NUM 1 int main() { pthread_t tid[NUM]; int i = 0; for(i = 0;i<NUM;++i) { pthread_create(tid+1,NULL,thread_run,(void*)&i); sleep(1); } printf("wait sub thread...\n"); //等5秒钟 sleep(5); printf("cancel sub thread...\n"); //指针变量可以当某个数据的容器 void *status = NULL; int ret = 0; //获得退出信息 ret = pthread_join(tid[0],&status); printf("ret: %d,status: %d\n",ret,(int)status); return 0; }
LPW的解释
在使用ps -aL查看线程情况时,LWP为内核LWP,我们最好不要叫它线程ID,因为在Linux中没有线程这玩意,我们所说的线程,都是进程PCB模拟出来的,属于轻量级进程。
对于LWP,它的值跟我们在测试代码时得出的结果(线程的ID)不一样,一个是原生线程库的,一个是内核的。
下面将好好分析一下,原生线程库中的"线程pid"的本质。
先来说结果,我们通过pthread_self()获取的线程id,其实是虚拟内存地址!
我们都知道,每一个线程都要有运行的临时数据,因此每个线程都要有自己的私有栈结构!也需要拥有描述线程的用户控制块!但是在虚拟地址空间中的栈结构,不可能会分成很多份给每一个线程的,它是属于主线程和进程的!
每一个新线程所拥有的栈结构等等,其实都是由原生线程库提供的!每一个线程跟每一个库提供的线程栈和线程局部存储等组成的用户控制块都是一一对应的,是以1:1的比例对对应着!
那么如何区找到需要找到的线程,就需要用到一个地址去找,并且每一个描述线程的用户控制块都会保存着每一个线程对应的PWD!这个地址就是每一个用户控制块的地址!
总结:
①pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和内核LWP不是一回事,前者是原生线程库中的,一个是内核LWP。
②前面讲的LWP(线程ID)属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
③pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
④线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID。