🍎作者:努力学习的少年
🍎个人简介:双非大二,一个正在自学c++和linux操作系统,写博客是总结知识,方便复习
🍎目标:进大厂
🍎 如果你觉得文章可以的话,麻烦你给我点个赞和关注,感谢你的关注!
📚 1. 线程和进程
**📚 **1.1 进程的基本概念
进程是分配系统资源的基本单位,当你运行程序的时候,系统就会给你创建进程,并为它分配各类资源,如进程地址空间,页表,进程控制块等资源,此时此时进程内部会伴随着一个执行控制序列让cpu去执行进程的代码和数据,这个进程最初的控制序列是进程内的主线程。
**📚 **1.2 线程的基本概念
线程是进程里面单一的执行控制序列(执行流),也是cpu调度的基本单位(分配给cpu执行最小单位),假如进程的运行是为了完成某个任务,线程是该任务的许多子任务之一,线程的创建,操作系统并不会给它分配内存资源,进程中所有线程共享所属进程地址空间上的资源。如果进程里只有一个线程,也就是只有一个线程在cpu中执行该进程的代码和数据。如果有多个线程,那么这些线程轮流在cpu中执行该进程的代码和数据。
引用知乎上的一个比喻:
1.单进程单线程:一个人在一个桌子上吃菜。
2.单进程多线程:多个人在同一个桌子上一起吃菜。所以多个人同时吃一道菜的时候容易发生争抢,也就是说资源共享就会发生冲突争抢
3.多进程单线程:多个人每个人在自己的桌子上吃菜。
一个线程只属于一个进程,一个进程可以拥有多个线程,线程之间可以并发执行。
在linux中,线程是在进程的基础改的,线程的控制块也是pcb(进程控制块是pcb),只是当我们在一个进程内部多创建一个线程的时候,不需要为线程创建进程地址空间,只需要多创建一个pcb,然后线程的pcb指向原本的进程所属的资源,这样线程就可以共享进程空间上的所有资源。
在linux内核中,是区分不出来进程还是线程的,因为线程和进程的控制块都是pcb。
单线程的进程:只有一个pcb
多线程的进程:多个pcb
栈区是主线程的私有栈,其它线程的私有栈在共享区域中的线程库里面,各个线程运行的代码都在代码区中,在动态库中,每个新创建的线程都有struct pthread和线程局部存储和线程的私有栈,struct pthread存储的是线程基本属性,线程的局部存储的是寄存器的数据。
**📚 1.3 **线程的优点
- 提高程序的并发性,例如我们在使用迅雷的时候,我们可以边看边下载,这个原理就是让迅雷其中一个线程去执行播放的任务,让一个线程去执行下载任务。
- 线程的创建比进程的创建开销小,在linux中,线程的创建只需要多创建一个pcb,然后指向所属进程的资源,而进程的创建,需要为进程创建pcb和进程的地址空间,页表和文件描述符等资源。
- 线程切换比进程切换要容易,在同一个进程中,线程只需要切换pcb即可,而进程的切换需要切换pcb和进程的地址空间,文件描述符等资源。
- 通信更加方便,因为进程内的所有线程共有一张进程地址空间,所以它们可以看到进程内中的大部分资源。而进程通信需要创建内核创建缓冲区(共享内存,管道等)进行通信。
**📚 **1.4 线程的缺点
- 线程不好调试(gdb支持不好,只能依赖程序员的经验)
- 不能支持信号。
- 若进程内的其中一个线程崩溃,则整个进程崩溃,例如,在迅雷内有一个线程在执行下载任务,一个线程在执行观看任务,其中下载任务的线程崩溃了,那么整个迅雷也就退出了。
**📚 1.5 **线程的私有资源
线程共享进程中的数据,但是它也有自己的数据:
- 私有栈;
- 寄存器;
- 线程ID;
- 调度的优先级;
**📚 1.6 **线程在进程共享的资源
线程除了共享所属进程的地址空间和页表外,还共享以下进程资源;
- 文件描述符表
- 每种信号的处理方式
- 当前用户的工作目录;
- 用户id和组id;
📚 2. 线程的基本操作
**📚 2.1 **线程的创建
每个进程创建的时候都会伴随着一个线程的产生,这个线程叫做主线程,如果我们想要在一个进程里面多创建一个新的线程去执行其它任务,那么我们可以用pthread_create进行创建。
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
thread:返回线程的id
attr:设置线程的属性,默认属性传入没NULL
start_routine:创建线程执行路线的起始函数,该函数默认的参数是void* 参数是void*
arg:传给 start_routine函数的参数。
void* routine(void* arg)//新线程的执行路线
{
int count=5;
while(count--)
{
printf("%s,pid:%d,ppid:%d\n",(char*)arg,getpid(),getppid());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,routine,(void*)"i am new thread");//主线程创建新的线程
while(1)//主线程执行路线
{
printf("i am main thread,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
创建新的线程后,主线程和新线程并发执行的,且新线程和主线程的进程pid都是一样的,说明他们都是在同一个进程内运行,只是执行的路线不同。
📚 2.2 ps -aL
查看LWP,每个线程都有一个LWP,这个LWP是cpu调度的基本单位,之前我们没学过线程,会认为pid是cpu调度基本单位,实际cpu调度是按LWP进行调度的。有些线程LWP不同,但是pid是相同的,这些线程同属于一个进程。
**📚 **2.3 线程退出
return
在线程的起始函数中遇到return,则该线程退出,线程调用的函数不算线程退出。如果是在main函数中return,则整个进程都退出。
void test()
{
int count=5;
while(count--)
{
printf("hello linux\n");
sleep(1);
}
return;//函数返回
}
void* routine(void* arg)//新线程的执行路线
{
pthread_detach(pthread_self());
int count=5;
test();//下面的代码不执行
while(count--)
{
printf("%s,pid:%d,ppid:%d\n",(char*)arg,getpid(),getppid());
sleep(1);
}
return (void*)0;//线程退出
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,routine,(void*)"i am new thread");//主线程创建新的线程
while(1)
{
printf("i am main thread,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(5);
}
return 0;
}
pthread_exit(void* value_ptr)
pthread_exit在线程调用的任何一个函数中执行都会使该线程退出。
void test()
{
int count=5;
while(count--)
{
printf("hello linux\n");
sleep(1);
}
pthread_exit((void*)10);//线程退出
}
void* routine(void* arg)//新线程的执行路线
{
pthread_detach(pthread_self());
int count=5;
test();//下面的代码不执行
while(count--)
{
printf("%s,pid:%d,ppid:%d\n",(char*)arg,getpid(),getppid());
sleep(1);
}
return (void*)0;
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,routine,(void*)"i am new thread");//主线程创建新的线程
while(1)
{
printf("i am main thread,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(5);
}
return 0;
}
**📚 2.4 ** 线程取消
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:取消线程,使该线程退出,可以让一个线程取消另一个线程,也可以线程自己取消自己。
被取消的线程的退出码是-1 .
void* routine(void* arg)//新线程的执行路线
{
while(1)
{
printf("%s,pid:%d,ppid:%d\n",(char*)arg,getpid(),getppid());
sleep(1);
}
return (void*)0;//线程退出
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,routine,(void*)"i am new thread");//主线程创建新的线程
int count=5;
while(1)
{
printf("i am main thread,pid:%d,ppid:%d\n",getpid(),getppid());
count--;
if(count==0)
{
void* ret;
pthread_cancel(id);//取消新线程
pthread_join(id,&ret);//获取新线程的退出码
printf("new join exit node: %d\n",(int)ret);
}
sleep(1);
}
return 0;
}
过程:
📚 2.5 线程的等待
线程等待的函数:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
thread: 等待线程的id;
retval:被等待的线程退出后的退出信息保存在retval,如果不关心可以设置为NULL。
线程等待是阻塞的,假设主线程在等待新线程退出并获取它的退出码之前,主线程是不会运行的。
接下来我们写一个程序,让主线程创建新线程并等待新线程:
void* routine(void* arg)//新线程的执行路线
{
int count=5;
while(count--)
{
printf("%s,pid:%d,ppid:%d\n",(char*)arg,getpid(),getppid());
sleep(1);
}
return (void*)0;
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,routine,(void*)"i am new thread");//主线程创建新的线程
void* retval;
pthread_join(id,&retval);//主线程等待新线程,将线程退出码保存在retval
printf("new thread exit: %d\n",(int)retval);
while(1)
{
printf("i am main thread,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
运行过程:
线程等待的必要性
- 新线程的创建是为了让它执行某个任务,当新线程退出后,就需要有一个线程等待是为了获取该线程的退出码,然后通过线程退出码,判断新线程的代码运行的结果是否正确,运行正确,则返回return 后面的值,或者pthread_exit()里面的值,若运行错误,则返回。
- 已经退出的线程,如果没有被其它线程等待获取它的退出码,那么其空间不会被释放,仍然在进程的地址空间内,创建新的线程不会复用刚才的线程的地址空间。当然,被分离的线程退出后会自动的释放空间,不需要被等待。
线程等待只能获得线程正常终止的退出码,为什么线程异常终止收到的信号不能通过线程等待来获取?
因为如果线程进行除0错误,或者指针越界等这些使线程异常退出是会使所属的进程退出,整个进程退出,那么进程内的所有线程都会被终止,那么其它线程就没有机会去获得线程的异常终止的信号。
📚 2.6 线程的分离
线程等待是阻塞的,假设主线程在等待新线程退出并获取它的退出码之前,主线程是不会运行的。
如果不关心线程的返回值,join是一种负担,那么我们可以将线程分离,即告诉操作系统,被分离的线程退出后自动释放资源。
#include <pthread.h>
int pthread_detach(pthread_t thread);
新线程的自我分离:
void* routine(void* arg)//新线程的执行路线
{
pthread_detach(pthread_self());//线程自我分离
int count=5;
while(count--)
{
printf("%s,pid:%d,ppid:%d\n",(char*)arg,getpid(),getppid());
sleep(1);
}
return (void*)0;
}
pthread_self() 获取自身的线程id
📚 3. 线程ID及进程地址空间布局
在用户层面的标识线程的id使pthread_t,而在之前说的LWP是cou进行调度线程的 基本单位,那么调度线程后,我们还需要知道该线程的私有栈位于在进程的地址空间上的哪个位置,这时候就需要有一个地址将它表示标识(这时linux的实现)。
对于linux而言的NPTL实现而言,pthread_t类型本质是进程地址空间上的一个虚拟地址。
NPTL是一个linux的一个原生线程库,该原生线程库是一个动态库,所有创建的新线程都是在这个动态库中维护,动态库通过页表映射到进程的地址空间上共享内存的区域上。线程pthread_t是线程在进程地址空间上的共享内存上的起始地址。pthread_t属于用户数据,因为它位于用户空间上的。每个线程可以通过pthread_t可以找到该线程的在进程地址空间上的起始位置,通过该起始地址,我们就可以找到新线程的struct_pthread,线程的局部存储,线性栈,struct_pthread是每个线程的私有栈,线程的局部存储保存的线程的上下文数据,线程栈是每个线程的私有栈。所以进程内部的切换线程,只需要将线程的cpu的上下文代码,和临时数据保存到线性局部存储,然后再共享内存上切换到一个线程。
**ldd test **查看test关联的动态库 ,test是关联libpthread.so.0这个动态库。
** 种一颗树最好是10年前,其次是现在!!**
感谢大家的观看,如果可以的话,麻烦点点关注和赞呗!
版权归原作者 努力学习的少年 所有, 如有侵权,请联系我们删除。