🔥🔥 欢迎来到小林的博客!!
🛰️博客主页:✈️林 子
🛰️博客专栏:✈️ Linux
🛰️社区 :✈️ 进步学堂
🛰️欢迎关注:👍点赞🙌收藏✍️留言
目录
- 线程的异常终止
- 线程等待
- 线程退出
- 线程取消
- 线程id
- pthread如何管理线程
- 线程分离
线程的异常终止
进程内部是可以存在多个线程的。那么如果有一个线程出现了异常(产生了信号)。那么会发生什么后果呢?我们用下面这段代码来验证一下。
#include<iostream>
#include<pthread.h>
#include<unistd.h> void* ThreadRoutine(void* args)
{int id = *(int*)args;delete (int*)args;int count = 0 ; while(1){printf("%d thread runing.... count = %d\n",id,count);//如果线程id为2, 且count计数到3,那么制造异常,产生信号 if(id == 2 && count == 3) {int a = 10;a /= 0; }sleep(1);count++;}
}int main()
{pthread_t tids[3]; //创建三个线程for(int i = 0 ; i < 3; i ++){int* id = new int(i);pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);}while(1){printf("main thread runing...\n");sleep(2);}return 0 ;
}
该代码的逻辑就是创建3个线程,让第三个线程计数到3的时候 /0 产生信号。
我们运行看看结果。
我们发现整个进程都崩溃了。所以可以得出结论:
一个线程产生异常,那么所有线程都会崩溃。本质是因为所有线程共享信号处理函数,而信号来临时选择了默认处理方式,那么就会干掉整个进程。进程都被干掉了,那么内部的线程肯定也无一幸免。
线程等待
在创建进程的时候,我们的父进程要等待子进程。否则子进程在结束时资源将无法释放,成为僵尸进程,造成内存泄漏。而线程这里也是一样的道理,如果主线程不等待子线程。子线程结束后,主线程还在运行,那么一样会造成资源的泄漏。所以等待子线程是很有必要的。
线程等待函数:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
第一个参数是要等待线程的tid
第二个参数是创建线程时传入的线程执行函数的返回值,在内核中会回调那个函数。而那个函数的返回值会返回给内核,再由内核输出到这个参数上。
返回值:返回0为成功,非0为错误码。如果线程已分离还进行join,那么会返回-1。
我们用以下代码来测试一下这个函数。
#include<iostream>
#include<pthread.h>
#include<unistd.h> void* ThreadRoutine(void* args)
{int id = *(int*)args;delete (int*)args;int count = 0 ; while(1){printf("%d thread runing.... count = %d\n",id,count);sleep(1);if(count++ == 3) break; }return (void*)id; //返回自己的id
}int main()
{pthread_t tids[3]; //创建三个线程for(int i = 0 ; i < 3; i ++){int* id = new int(i);pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);}//线程等待int* ids[3] = {0};pthread_join(tids[0],(void**)ids+0); pthread_join(tids[1],(void**)ids+1); pthread_join(tids[2],(void**)ids+2); //打印三个线程的返回结果for(int i = 0 ; i < 3 ; i++)printf("thread %d : %ld\n",i,(long long)(ids[i]));return 0 ;
}
这段代码的逻辑就是 创建三个线程,每个线程执行三次循环,随后返回传入时的id。主线程等待三个线程,等待结束后打印三个线程的返回值。
运行结果:
线程退出
如果我们在线程内调用exit函数,那么整个进程都会退出。因为exit是进程退出函数,无论在哪调用,都会导致进程退出。进程退出,那么所有的线程也会被释放。
所以线程退出,我们要用指定的退出函数。
线程退出函数:
#include <pthread.h>
void pthread_exit(void *retval);
参数是传入一个当前线程的返回值。
那么我们用代码来演示一下程序退出。
#include<iostream>
#include<pthread.h>
#include<unistd.h> void* ThreadRoutine(void* args)
{int id = *(int*)args;delete (int*)args;pthread_exit((void*)(id + 10));int count = 0 ; while(1){printf("%d thread runing.... count = %d\n",id,count);sleep(1);if(count++ == 3) break; }return (void*)id; //返回自己的id
}int main()
{pthread_t tids[3]; //创建三个线程for(int i = 0 ; i < 3; i ++){int* id = new int(i);pthread_create(tids+i , nullptr, ThreadRoutine,(void*)id);}//线程等待int* ids[3] = {0};pthread_join(tids[0],(void**)ids+0); pthread_join(tids[1],(void**)ids+1); pthread_join(tids[2],(void**)ids+2); //打印三个线程的返回结果for(int i = 0 ; i < 3 ; i++)printf("thread %d : %ld\n",i,(long long)(ids[i]));return 0 ;
}
代码逻辑很简单,一进线程就退出,传入的值为 传入的id+10。
运行结果:
线程取消
线程退出函数必须要在要退出的线程内部执行。但如果我想在主线程中让指定的线程退出。那么我们可以用线程取消的函数。
线程取消函数:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
传入的是要取消的线程tid。
返回值为0则成功,失败返回一个错误的非0数字。
我们用一段代码来验证这个函数。
#include<iostream>
#include<pthread.h>
#include<unistd.h> void* ThreadRoutine(void* args)
{int count = 0 ; while(1){printf("%s runing.... count = %d\n",(char*)args,count);sleep(1);}return nullptr; //返回自己的id
}int main()
{pthread_t tid; pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");sleep(3); pthread_cancel(tid); //取消线程 sleep(3);return 0 ;
}
这段代码逻辑很简单,创建一个线程死循环执行。主线程三秒之后取消这个线程。再过三秒之后主线程结束(整个进程结束)。 在这个过程中我们实时监视。
监视结果:
我们发现三面之前这个进程有2个线程,三秒之后就变成了一个线程。说明创建的线程被取消了,而三秒之后一个线程也没有了,说明主线程执行完毕,进程退出了。
线程id
我们可以用pthread_self()来返回线程id,并分别用16进制和10进制打印这个id。因为我提前知道了这个id会是个很大数(透剧怪登)。
#include <pthread.h>
pthread_t pthread_self(void);
那么我们写段代码来打印一下线程的id。
#include<iostream>
#include<pthread.h>
#include<unistd.h> void* ThreadRoutine(void* args)
{int count = 0 ; pthread_t tid = pthread_self();while(1){printf("%s runing.... count = %d ,thread id %ld , %lX \n",(char*)args,count,tid,tid);sleep(1);}return nullptr; //返回自己的id
}int main()
{pthread_t tid; pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");pthread_join(tid,nullptr) ; return 0 ;
}
这段代码的逻辑就是创建一个线程,然后通过pthread_self()函数获取当前线程的id,随后分别以10进制和16进制打印这个id。
运行结果:
当10进制打印进程id时,我们会发现它是一个很大的数字,并且看不出什么,但是当我们以16进制打印时。我们惊奇的发现,这是不是很像一个地址?
没错,线程id就是一个地址!
那么就有疑问了,CPU不是LWP进行调度吗?为什么线程id不是LWP,却是一个很大的数字?我们要搞清楚一点,pthread库是一个动态库!!而LWP是系统内核管理线程的标识。线程id是pthread库管理线程的标识。
在内核方面,用LWP来标识PCB进行调度,所以没有进程线程的区别。但是不要忘记了,线程是用pthread库创建出来的。那么创建出来要不要进行管理?这是必须的!!
pthread如何管理线程
首先我们都知道pthread是一个动态库,那么在程序执行的时候。会先把动态库加载进内存,随后根据页表把它映射到进程的共享空间内。
当pthread_create创建了一个线程时,那么就会在动态库中执行这个函数。随后就会创建一个struct pthread的结构体,线程局部存储以及线程栈。而这个结构体的地址(共享区中的虚拟地址)就是线程的id。这个结构体就是对这个线程的描述,存储着线程的信息,用来管理该线程。
虽然进程内部的线程都是共享进程地址空间的,那么就意味着进程地址空间中的栈(也叫主线程栈)可以被所有的线程访问。这是必然的,但访问归访问。线程本身的数据是不能存储在主线程栈中的。因为这样会导致存储十分混乱。也不能一个一个线程进行覆盖,这样会导致数据缺失。所以pthread动态库为每一个线程都提供了一个线程栈,这个线程栈是每个线程独有的(想跨线程访问也可以访问,但还是别这样做)。
线程局部存储是什么呢?
我们可以用__thread 来让一个全局变量变成线程私有的。
先看一段代码:
#include<iostream>
#include<pthread.h>
#include<unistd.h> int val = 0;void* ThreadRoutine(void* args)
{while(1){printf("new thread , val = %d , &val = %p \n",val,&val);sleep(1);}return nullptr; //返回自己的id
}int main()
{pthread_t tid; pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");while(1){printf("main thread , val = %d, &val = %p\n",val,&val);val++;sleep(1);}pthread_join(tid,nullptr) ; return 0 ;
}
这段代码的逻辑就是创建一个线程不断打印全局变量,主线程也不断打印全局变量,但是主线程会在每打印一次自增一次全局变量。
这段代码的结果是这样的:
没有意外,因为线程之间共享全局变量。所以打印的地址是一样的,值也是同步的。
那么我们把int val换成__thread int val再来试试效果。
#include<iostream>
#include<pthread.h>
#include<unistd.h> int val = 0;void* ThreadRoutine(void* args)
{while(1){printf("new thread , val = %d , &val = %p \n",val,&val);sleep(1);}return nullptr; //返回自己的id
}int main()
{pthread_t tid; pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");while(1){printf("main thread , val = %d, &val = %p\n",val,&val);val++;sleep(1);}pthread_join(tid,nullptr) ; return 0 ;
}
然后我们运行这段程序。
运行结果:
我们发现两个val的值不同步了,地址也不一样了。
原理很简单,本质就是在编译时,把val值的数据拷贝了一份。拷贝进了对应线程的线程局部存储中。
结论:
线程id本质是一个地址,这个地址在共享区中,与页表建立映射。最终映射到物理内存中对应的struct pthread结构体的地址起始处。在pthread_create创建了一个线程之后,pthread会在共享区创建一块内存。这个内存存储着线程的结构体,线程的局部存储以及线程栈。 而通过线程id可以找到线程结构体的起始位置。
线程分离
如果我们的主线程创建了线程之后却不想管它。那么我们可以用pthread_detach来让线程分离。 用pthread_detach来分离线程,那么被分离的线程在结束后就会自动销毁。如果不detach也不join,那么线程的一些资源就无法被释放,此时的线程就会陷入与僵尸进程相似的状态。
线程分离函数:
int pthread_detach(pthread_t thread);
参数是传入要分离线程的tid。
返回值:0为成功,非0则分离失败。
pthread_detach代码:
#include<iostream>
#include<pthread.h>
#include<unistd.h> void* ThreadRoutine(void* args)
{pthread_detach(pthread_self());int count = 5;while(count--){printf("new thread , count = %d\n",count);sleep(1);}return nullptr; //返回自己的id
}int main()
{pthread_t tid; pthread_create(&tid , nullptr, ThreadRoutine,(void*)"new thread ");while(1){printf("main thread runing.... \n");sleep(1);}return 0 ;
}