线程的认识
在之前的学习中我们知道当一个程序加载进物理内存的时候操作系统会为该程序创建对应的PCB,进程地址空间,页表来管理这个程序:
其中虚拟地址空间在向物理内存进行转换的过程不仅需要软件页表来支持还得需要硬件MMU来支持,之所以采用软硬件结合的方式来实现是因为,在计算机当中硬件的速度是远远大于软件的,因为软件的执行涉及到内存到cpu的来回迁移但是硬件却不需要,但是硬件存在一个缺陷就是一旦硬件设计好了,那么他的可扩展性就变得很低,可维护成本变得很高,所以这个时候使用软件就可以很灵活的进行调整并修改,那么这就弥补了硬件的缺点所以在虚拟地址空间转换到物理内存的时候软件就用来维护页表,硬件就用来实现虚拟地址到物理地址的转换,所以我们之前说的进程就是指的内核数据结构+进程对应的代码和数据。那什么是线程呢?大家肯定在各种教材上都看到了这么个概念说:线程就是进程内部的一个执行流,这确实是线程的概念但是这个概念太宏观了对于很多操作系统中的线程都适用,以至于这个概念显得十分的空洞不好理解,那么来我站在linux的角度来带着大家理解线程,线程确实是进程内的一个执行流,但是不同的平台多线程的实现方式是不一样的,所以我们这里就谈谈linux操作系统中线程的实现方式。linux在创建子进程的时候会给子进程单独创建一个task_struct对象,虚拟地址空间和页表,并且父进程和子进程在没有发生写时拷贝的时候共享的还是内存上的同一份资源,而我们知道虚拟内存决定了进程能够看到的资源,这就好比进程是一个在房子里面的人,虚拟内存则是房子上的窗户,那么窗户有多大就决定着里面的人能够看到多少的风景,目前一个进程所能看到的主体资源是由地址空间决定的,而通过地址空间加上页表就访问到进程载入到内存上的数据资源和代码资源,而之前我们学习过进程可以将自己的代码划分成为几个区域,然后让其他的执行流来执行,比如说通过fork创建子进程然后根据fork函数的返回值来让父子进程执行不同区域的代码,虽然创建父子进程可以让不同执行流执行相同代码的不同区域,但是创建子进程的时候不仅得创建PCB还得创建虚拟地址空间和页表
如果以这样的方式来实现多执行流执行不同区域的代码的话浪费的资源是不是就有点多啊,虽然执行的是不同区域的代码但是多多少少还是有些资源是会共用的,虽然是两个执行流但是父子进程在不对公共资源进行修改的前提下页表两边的映射也是一样的,那虚拟地址空间和页表的创建是不是就显得有些多余啊,所以这里我们就想了另外一个方法,我们对虚拟地址空间进行划分,再通过页表将不同的区域进行不同的映射,比如说将虚拟地址空间中的代码区划分成为A B C D四个区域,其中AB区域让task_struct1来执行,然后执行CD区域的代码时就不需要再创建页表和虚拟地址空间而是只创建task_struct对象让其执行CD区域的代码就行,这样一个进程在执行代码的时候就会存在多个PCB,这些PCB都指向了同一个虚拟地址空间,比如说下面的图片:
那么我们就把只创建pcb然后从父进程中分配资源的这种执行流称为线程,这里大家明白一个共识:一个进程所对应的资源是可以通过虚拟地址空间加上页表来将自己的部分资源划分给特定的线程的,这就好比之前创建子进程的时候是可以通过if else来让父子进程执行不同的代码的,因为我们可以通过虚拟地址空间+页表的方式对进程进行资源划分,所以单个进程的执行粒度一定要比之前的进程要细,那么这就是线程的初步理解。
线程的管理
在上面我们说过线程是进程内的一个执行流,那么这个执行就一定是cpu在执行,所以这个概念就给我们一个感觉:cpu好像要区分哪个是线程哪个是进程,执行线程的时候得以线程的方法来执行,执行进程的时候得以进程的方式来执行,所以不管怎么样只要我们在进程的前面加上了一个线程的概念,那么操作系统中就可能存在多个线程,那我们要不要对线程进行管理呢?答案肯定需要的,而管理的方法就是先描述再组织,所以操作系统为了描述操作系统就一定会给线程设计专门的数据结构来表示线程对象,这个数据结构就称为TCB,而windows操作系统中就采用的这样的方法,但是采用这样嗯方法就存在一个问题,进程要维护进程之间的父子关系和兄弟关系,还要保证进程能被cpu调度,并且进程的内部还会有多个线程,那么进程和线程的关系也要被维护,线程和线程的关系也要被维护,进程要被调用线程也要被调用,那么这就导致线程控制块的设计和进程控制块的设计耦合度特别高而且非常复杂,而一个线程被创建出来就是为了被执行的,被调度的,既然要被调度的话那么线程肯定就得有id,状态,优先级,上下文,栈等等…所以单纯从进程和线程的调度角度来看的话,线程和进程有很多地方都是重叠的,那对于cpu来说线程和进程的调用有区别吗?答案是没有的,站在cpu的角度来看他不管执行的代码是父进程还是父进程创建的线程,只要给cpu一个pcb它就能直接运行,而linux的工程师他们也不想给linux的线程专门设计对应的数据结构而是直接复用PCB,用PCB来表示linux中的线程,这样就简化了线程的复杂度,那么这就是linux中的线程的管理,所以现在对线程的理解就可以是:线程在进程的内部执行,线程在进程的地址空间内运行,拥有该进程的一部分资源。
进程和线程的区别
在之前的学习过程中我们知道进程指的就是加载进内存的程序,以及操作系统为该程序创建的内核数据结构比如说PCB,页表,虚拟地址空间等等,我们把这所有加在一起称为进程:
那现在学习的线程呢?他也有自己的PCB虽然与其他PCB共用同一个虚拟地址空间和页表但是不代表他没有啊,所以线程和进程的概念冲突吗?答案是不冲突的
之前学习的进程=内核数据结构+进程对应的代码和数据,那现在学习的进程站在内核的视角来看就是承担分配系统资源的基本实体,比如说我们把程序加载进内存的时候操作系统会创建堆的PCB,一个虚拟地址空间,一堆的页表,以及把程序加载进内存的这个过程有没有消耗操作cpu的资源来帮你创建并初始化呢?有没有消耗内存的资源来帮你存储数据呢?有没有消耗cpu的io资源帮你从外设中加载进内存的呢?答案肯定是有的,那么我们就把所以支出的这些资源的整体称为进程,所以站在内核的角度来看进程就是承担分配系统资源的基本实体,在之前的理解中我们可能认为进程的概念就是一堆的PCB+一个虚拟地址空间+页表+物理内存的一部分就称为进程
那么现在我们理解的进程就得是创建进程的时候操作系统得创建对应的内核数据结构来占有一部分资源,进程对应的代码和数据也得占用一部分的资源,还得占用cpu资源io资源等等资源,我们这占用的一堆资源称之为进程,那什么是线程呢?线程的概念就是cpu调度的基本单位,可是之前的学习过程中认为cpu调度的基本单位是进程啊,那现在你告诉调度的基本单位是线程那之前的讲的是不是有问题了呢?答案是没有问题的,之前讲的进程也是承担系统资源的基本实体只不过内部只有一个执行流也就是一个线程,比如说下面的图片:
对于这样的进程cpu调度其内部的线程和调度整个进程有差别吗?答案是没有差别而今天讲的进程内部存在多个执行流他包括了以前讲的只有一个执行流的情况,所以之前讲的和现在的相比只是一个子集,那么我们把下面图片的蓝色部分就可以看成是线程,他是cpu调度的基本单位:
所以现在讲的线程在内核层面来看只需要在以前进程的框架下创建对应的PCB然后把线程中的资源给其指派一点即可,然后cpu在调度的时候他是直接拿一个PCB然后就开始执行的,他不会区分当前调度的对象是一个线程还是一个进程他也区分不了,所以站在cpu的角度来看之前学习的时候他调度的是一个进程,而现在来看他调度的可能是进程内的一个分支,但是cpu并不关心这两的区别他只需要能够正常的执行就行,所以今天cpu调度的PCB量级一定是小于等于之前CPU调度的PCB量级的,比如说操作系统里面有100个PCB但是只有10个进程其中只有一个进程里面有一个PCB,那么剩下的99个PCB都属于剩下的9个进程,所以现在学习的PCB的量级一定小于等于之前学习的PCB的量级,所以现在cpu调度PCB的时候我们都将这个PCB称之为轻量级线程而不管他是属于之前的进程还是现在的线程都将其称为轻量级线程。那么学习到这里我们可以得到以下几个结论:
1.linux内核中是没有真正意义的线程的,linux是用进程PCB来模拟的线程的,是一种完全属于自己的一套方案。
2.站在CPU的视角,每一个PCB都可以称之为轻量级进程。
3.linux线程是cpu调度的基本单位,而进程是承担分配系统资源的基本单位
4.进程用来整体的申请资源,线程用来伸手向进程要资源。所以线程在向操作系统申请资源的时候本质上也是进程向操作系统要资源,线程出现问题也代表着进程出现了问题,比如说公司里面分为很多个小组,小组里面有个人向公司要资金要设备的时候是这个人在要吗?肯定不是的是这个人代表的小组向公司申请资源,而一个人做的一些事情导致了一系列的损失是这个人负责吗?肯定不是的是这个人代表的小组一起负责。
5.linux中没有真正意义的线程他没有专门创建一个数据结构来描述线程,他是用PCB来模拟的线程,但是linux中还是依然存在着线程的概念。
6.linux之所以使用PCB模拟线程是因为之前实现的PCB的概念和算法都可以用上了,不用创建新的数据结构,降低了维护成本可靠高效,直接将之前的东西全部平移过来就行了降低了设计的复杂度
为什么要有多线程
这里通过两个例子带着大家理解多线程,一个是实际应用的例子一个是生活上的例子,首先是实际的例子:大家肯定使用过迅雷下载器下载电影,并且迅雷在下载电影的过程中还可以观看电影,但是下载电影和观看电影是两个不同的任务,但是迅雷这个应用是一个进程,所以这两个任务在进程内只能被串联的访问,就好比main函数不能同时访问两个函数执行两个函数的内容,而是只能执行完一个函数之后再执行另外一个函数,那么这里的函数就可以看成任务,但是有了多线程就不一样了,我们可以让一个线程执行下载任务让另外一个线程执行播放电影的任务,虽然在cpu的视角看到的都是两个独立的task_struct,但是cpu通过其高速的切换给你的感觉就是这两个任务在同时的运行,而这两个任务又恰好只能在迅雷这一个进程里面执行,因为这两个功能必须是迅雷这一家公司实现的,不能说迅雷在下载电影的同时我们还能用腾讯视屏来播放这个电影,这个是不行的因为迅雷在下载电影过程中下载的资源对于其他进程来说是不可访问的,那么这就是多线程的作用。那么接下来我们来看一个生活中的例子,工厂大家肯定都见过,工厂中的工人在生产产品的时候每个人往往都是一个固定的位置,在固定的位置干着同样的事情,不同位置的工人往往做的事情是不一样的,比如说制作一个产品需要的步骤有5步,那么我们就把工人分为5种每种工人就完成对应的生产任务就行,虽然这些工人能够完成这5步的任何一步但是我们从来没有见过哪个工厂让每个工人都按照顺序来从0到1的生产一个完整的产品的,因为这么做不仅效率低而且出现问题的概率还很高,那么多线程也是这样的思想将整个进程看成一个工厂,其中的每个线程就看成工程中的工人,每个工人坐着自己对应的事情这样分工明确既可以提供程序的效率还可以降低出错的概率,虽然不同的工人做着不同的事情但是这些事情汇总到一起就是让产品更好,虽然一个进程里面存在着多个线程,每个线程执行不同的任务,但是这些任务和在一起的目的就是程序运行的跟快稳定性更强,那么这就是多线程的意义。
线程的特性
操作系统只认线程,而且用户(程序员)也只认线程,但是通过上面的讲解我们知道linux这边是没有线程这个东西的,他是用PCB来模拟的线程并且将这个PCB称之为轻量级进程,所以linux操作系统是不认线程这个概念的
也就是说linux无法直接提供创建线程的系统调用接口,而只能给我们提供创建轻量级进程的接口,所以为了解决用户和linux中的这层差异就有大佬实现了一个库,这个库叫做线程库他可以解决轻量级进程和线程之间的转换问题。
多线程的创建和证明
首先我们来认识一个函数:
pthread_creat(pthread_t *thread,const pthread_attr_t *attr,void*(*start_routine)(void *),void *arg)
第一个参数表示线程的id他是一个输出型参数,传递一个pthread_t类型的指针过来他会将线程的id设置进这个变量里面,第二个参数表示线程的属性这个参数不用管直接设置为空即可,第三个参数表示函数指针也就是创建的线程所执行的函数通过这个参数就可以将主线程的一部分资源肢解开给新创建出来的线程,第四个参数就是执行函数的参数,函数的返回值是一个整形,如果线程创建成功了这个函数就返回0,如果创建失败就返回对应的错误码,那么接下来我们就创建一个函数函数的内容就是执行一个循环,然后在主线程里面再创建一个循环,这样将程序运行起来之后如果线程创建成功了就可以看到有两个循环在一起运行,如果没有线程没有创建成功就只能看到一个循环在运行,因为一个执行流是不能同时执行两个死循环的那么这里的代码如下:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
void* thread_one(void *args)
{
while(true)
{
cout<<"我是新线程"<<endl;
sleep(1);
}
}
int main()
{
pthread_t t_id;
pthread_create(&t_id,nullptr,thread_one,(void*)"thread one");
while(true)
{
cout<<"我是主线程"<<endl;
sleep(1);
}
return 0;
}
将程序运行起来就可以看到这样的现象:
这里报错的原因是说没有找到对应的函数pthread_create,可是我们明明包含了头文件pthread.h啊,为什么会找不到这个函数呢?原因很简单因为使用的第三方库pthread.h,所以在生成可执行程序的时候得告诉编译器库在哪里,操作系统认线程,用户也认线程,但是linux操作系统无法直接提供创建线程的系统调用接口,只能提供创建轻量级进程的接口,所以用户创建线程的时候不能直接访问操作系统的接口,所以操作系统就在用户和操作系统接口直接提供了一个程序员实现的用户级线程库,也就是在用户和系统调用直接做出了一个封装,对上向用户提供了创建线程的接口,然后在库里面就会讲创建线程的操作转换称为创建轻量级进程的操作,这个库是任何linux操作系统都必须默认携带的第三方库,所以在编译的时候我们得添加一些指令-L是告诉操作系统库在哪里,-I是告诉操作系统库的头文件在哪里,但是这个库在出厂时就自带了,所以他大概率他的会在操作系统指定的路径下,所以现在只需要告诉操作系统库的名字是是什么,所以这个时候我们就只需要加上一个-l选项库的名字叫做pthread,那么这里makefile的代码就如下:
代码的运行结果如下:
可以看到当前执行了一个进程但是却有两个执行流在运行,那么这就说明线程创建成功了。通过ldd指令我们可以看到生成的可执行程序所依赖的库:
可以看到当前的进程确实依赖了一个名为pthread库,并且这个库在/lib.64/路劲下:
可以看到这就是我们所依赖的线程库也就是原生线程库,在这个程序运行的过程中我们可以使用ps ajx指令查找指定的进程:
通过查找我们只能看到一个mytest正在执行,可是根据之前的学习我们知道这里应该是两个轻量级进程正在执行啊,那么要想看到两个进程我们就得使用ps -aL选项,a表示all L表示的就是轻量级进程,那么这里显示的内容就如下:
可以看到这里显示了两个mytest,那么这就表明当前的操作系统里面存在两个mytest的轻量级进程,这两个mytest的PID都是一样的都是14417,说明这两个执行流都属于同一个进程但是他们还有一个东西叫做LWP,两个mytest的LWP是不同的LWP的全称是light weight process 也就是轻量级进程ID ,如果PID和LWP相同的就表明是主线程,如果不一样就表明他是新创建出来的线程,所以cpu调度的时候是以LWP来表示特定的执行流不是用pid来标定执行流的,而我们之前学习的时候说进程调度的时候是以pid来标定执行流的那这是不是就是错的呢?当然不是的当进程里面只有一个执行流的时该执行流的PID和LWP是一样的,而我们之前学的都是只有一个执行流的情况所以是没有问题的。那么接下来我们再来看看这个函数的参数,我们说创建线程的时候还可以给函数传递参0数,那这个怎么看呢?方法也是非常的简单函数使用void*类型的参数来接收参数,那么在函数里面我们就可以将接收的参数进行转换,转换成为你想要的类型,然后就可以使用这个参数,比如说下面的代码:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
void* thread_one(void *args)
{
const char* name =(const char *) args;
while(true)
{
cout<<"我是新线程,我的名字是:"<<name<<endl;
sleep(1);
}
}
int main()
{
pthread_t t_id;
pthread_create(&t_id,nullptr,thread_one,(void*)"thread one");
while(true)
{
cout<<"我是主线程"<<endl;
sleep(1);
}
return 0;
}
代码的运行结果如下:
可以看到这里的参数就排上了用场,当然这里还可以传递更加复杂的参数那么这里只是见见猪跑我们以后再讲。接下来我们再来看看函数的第一个参数,我们说他是一个输出型参数,那这个参数是什么样子的呢?里面装的是什么内容呢?是不是刚刚查看的LWP呢?那么接下来我们就可以用下面的代码来进行验证:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
void* thread_one(void *args)
{
const char* name =(const char *) args;
while(true)
{
cout<<"我是新线程,我的名字是:"<<name<<endl;
sleep(1);
}
}
int main()
{
pthread_t t_id;
pthread_create(&t_id,nullptr,thread_one,(void*)"thread one");
while(true)
{
cout<<"我是主线程,我创建出来的tid: "<<t_id<<endl;
sleep(1);
}
return 0;
}
代码的运行结果如下:
ps -aL的执行结果如下:
可以看到这里的输出型参数里面记录的不是线程的LWP,那他会是什么呢?我们再以16进制的方式打印一下tid的内容:
int main()
{
pthread_t t_id;
pthread_create(&t_id,nullptr,thread_one,(void*)"thread one");
while(true)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%x",t_id);
cout<<"我是主线程,我创建出来的tid: "<<buffer<<endl;
sleep(1);
}
return 0;
}
代码的运行结果如下:
可以看到以16进制的方式打印tid的内容也不是LWP但是tid的内容看起来好像是一个地址,实际上他就是一个地址但是是什么地址这里我们就先放一放以后再聊。
线程特性的补充
1.线程一旦被创建几乎所有的资源都是被线程共享的,比如说我们创建了一个函数,那么主线程可以调用这个函数,新创建出来的子进程也可以调用这个函数,比如说下面的代码:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
string func()
{
return "我是一个独立的方法";
}
void* thread_one(void *args)
{
const char* name =(const char *) args;
while(true)
{
cout<<"我是新线程,我的名字是:"<<name<<func()<<endl;
sleep(1);
}
}
int main()
{
pthread_t t_id;
pthread_create(&t_id,nullptr,thread_one,(void*)"thread one");
while(true)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%x",t_id);
cout<<"我是主线程,我创建出来的tid: "<<buffer<<func()<<endl;
sleep(1);
}
return 0;
}
代码的运行结果如下:
可以看到这里的主线程和新线程都可以正常的调用这个函数,并且不只是函数的调用对于全局变量主线程和新线程也是都可以访问并修改的,比如说下面的代码:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
int num=10;
void* thread_one(void *args)
{
const char* name =(const char *) args;
while(true)
{
cout<<"我是新线程,我的名字是:"<<name<<"num"<<":"<<num<<endl;
sleep(1);
}
}
int main()
{
pthread_t t_id;
pthread_create(&t_id,nullptr,thread_one,(void*)"thread one");
while(true)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"0x%x",t_id);
++num;
cout<<"我是主线程,我创建出来的tid: "<<buffer<<"num的值"<<":"<<num<<endl;
sleep(1);
}
return 0;
}
新线程访问全局变量,主线程也访问全局变量但是他还会修改全局变量,所以这里就可以看到主线程和新线程访问全局变量的值是一样的,并且还会随之改变,那么代码的运行结果如下:
这就是线程的特性之一,进程中的几乎所有的资源都是被所有的线程所共享的,既然是这样的话那么在线程中实现通信就会变得十分的容易。
第二点:
虽然线程大部分资源都是共享的,但是线程也一定要有自己私有的资源,就好比一个家庭里面很多的东西所有成员都是可以使用,比如说零食,电视,汽车,冰箱等等等,但是总有那么一些东西是每个成员私有的比如说爸爸的私房钱,妈妈的化妆品,孩子的那个没有及格的成绩单,所以资源中的哪些是每个线程私有的呢?
第一个:线程肯定是要被cpu调度,那么他一定得有对应的PCB属性,所以这些属性一定是每个线程私有的不能被其他的线程所访问和修改。
第二个:线程是要被切换,那么线程在被切换的时候可能并没有执行完,没执行完就存在上下文结构,那么上下文结构就一定是每个线程私有的。
第三个:通过上面的代码我们可以看到主线程在执行一段代码,新线程在执行一个函数,等未来我们创建更多的线程时就会出现多个线程执行多个不同函数的情况,那么这些函数在执行的时候可能会产生临时数据,这些临时数据是每个线程的函数自己产生的其他线程大概率用不上也用不了所以这些临时数据就得有个地方进行存储,存储的地方就是每个线程所独有的栈上,所以每个线程都会有独立的栈结构,那么这里就有个问题根据上面描述线程的图片我们可以看到栈区只有一个啊,而且我们知道栈区是通过两个寄存器来表示栈顶和栈底的,可是一个cpu中只有一套寄存器啊那如何来表示每个线程中的独立栈结构呢?这些独立的栈结构又是存储在哪的呢?那么这些问题我们后面会解答。
线程的优点
1.创建一个新线程的代价要比创建一个新进程的代价小很多。第一:进程之间的切换需要切换页表,切换PCB,上下文,进程地址空间,而线程之间的切换只需要切换PCB和上下文。第二:cpu中除了拥有各种类型的寄存器,还存在一个硬件级别的catch,这个东西的速度要比内存快,但是比寄存器要慢,也就是高速缓存,他有数据的保存能力,因为软件存在一个局部性原理,访问一个位置的数据时,该数据旁边的数据也会大概率被访问到,所以就会讲这些可能的数据存放到cache里面,这样cpu在读取数据的时候就不会先到内存中进行读取,而是先去catch中进行读取,如果catch中不存在的话再去内存中读取,所以一个运行稳定的进程,catch中一定存放了很多热点数据,又因为线程之间的数据本来就是共享的,所以线程切换的时候catch中的热点数据依然实用不需要被切换,但是进程之间切换会导致之前缓存到catch中的数据失效,进程切换之后又得重新将内存中的数据加载进catch里面,所以线程的切换cache不用太更新,但是进程的切换得全部更新,这就导致效率降低。
2.线程占用的资源要比进程少很多
3.能充分利用多处理器的可并行数量,这里进程也有这个特点但是线程的这个有点更加的明显。
4.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
5.计算密集型应用(主要占用cpu的资源比如说:加密,解密,算法等)为了能在多处理器系统上更高效的运行这些应用,可以将这些计算分解到多个线程中实现。
6.I/O密集型应用(通过I/O访问外设的应用,比如说访问磁盘,显示器,网络)),为了提高I/O效率就可以将I/O操作重叠,线程可以同时等待不同的I/O操作(比如说迅雷下载电影,如果网络特别好就可以同时下载10部电影
线程的缺点
1.性能损失
线程的个数最好和cpu的数目相同,一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2.健壮性降低
一个进程出现了问题,不会影响其他进程,但是一个线程挂了会影响其他的线程,编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
3.缺乏访问控制
进程是访问控制的基本粒度,线程中调用函数或者使用变量可能会对整个进程造成影响,就比如说我们上面的代码,两个线程都可以访问全局变量和函数,那如果某些线程不希望某些全局变量改变,但是有一些线程不小心将其改变了就会出现一些问题,而线程之间共享的东西还很多比如说:文件描述符表,每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数),当前工作目录,用户id和组id等等
4.编程难度高
编写与调试一个多线程程序比单线程程序困难得多
线程的健壮性问题
当进程中的某个线程出现了问题报出异常被终止,那么进程中的其他线程也会跟着出现问题然后被终止,这就称为健壮性或者鲁棒性较差,之所以出现这样的问题是因为信号是整体发送给进程的,这就好比小组的某个人出现问题导致了公司出现了损失,但是公司不会怪罪一个人而是怪罪这个人所在的整个小组,而小组就相当于是进程,而小组的某个成员就相当于是线程,进程负责向操作系统申请资源,线程是进程的一部分,所以线程就代表着进程,所以线程出现问题就代表着进程出现了问题,进程出现问题操作系统就会讲进程的资源进行回收,进程的资源被回收了就代表着线程的资源也都被回收了,所以线程出现了问题这个进程的所有线程也就跟着结束,但是进程不会这样子进程出现了问题并不影响父进程。
clone函数
clone函数就是操作系统提供的创建轻量级进程的接口,该函数的声明如下:
fork函数底层使用的就是这个接口,该函数既能创建轻量级进程也能创建子进程,第一个参数就是一个函数指针也就是新的执行流要执行的代码,第二个参数表示的是子栈这个参数就跟之前提到的内部栈相关,除了fork函数外,操作系统中存在一个函数名为vfork,该函数的声明如下:
这个函数也可以创建的子进程,但是该子进程和父进程共享同一个地址空间所以该子进程就是轻量级进程,这里大家了解一下就行,我们使用线程库中的函数在底层上也是调用的clone函数来创建的线程,你认为的线程也会通过clone转化成为轻量级进程,那么本篇文章的全部内容就结束,在下一篇文章里我们会继续了解一下线程中的栈结构也就是clone函数的第二个参数。