进程与线程
操作系统
操作系统是一个软件,~~(由代码构成程序)
操作系统的职责:
1.管理硬件各种设备
2.给其他软件提供稳定的运行环境~~
将上面这两种功能进行抽象和封装
进程/任务(Process/Task)
进程就是操作系统,提供的一种"软件资源"
目前市面上的系统都是多任务操作系统,同一时刻可以同时运行多个任务.
与之相对应的是单任务操作系统,只能在同一时刻运行同一个程序
我们所说的进程主要是对于多任务系统来说的.
每个任务/进程在执行过程中,都要消耗一定的硬件资源
换而言之,计算机的每个进程,在运行的时候,都应该分配一定的系统资源
进程是系统分配资源的基本单位
操作系统中的进程管理
1.先描述(先试用类/结构体这样的方式,把实体属性全部列举出来)
表示进程信息的结构体,称为PCB(进程控制块,Process Control Block)
具体在Linux上,PCB是一个叫做struct task_struct{........}这样的结构体
2.再组织(使用一定的数据结构,将这些类/结构体/对象,连接在一起)
在Linux中,使用链表的数据结构将若干个task_struct给串起来.~~
当我们看到任务管理器中的这些进程的时候,意味着系统内部就在遍历链表,并且打印每个节点的相关信息;
如果运行一个新的程序,系统就会多出一个进程,多的这个进程就需要构造出一个新的pcb,并且添加到链表上.
如果某个运行中的程序,退出了,就需要把对应进程的pcb从链表中删除,并且销毁对应的pcb资源.
PCB这个结构体,有上百个属性.
接下来简单介绍一下最核心的属性
pid
进程的身份标识
系统会保证同一台机器,同一个时刻,每个进程的pid都是唯一的.
(后续再对进程进行操作的时候,只需要操作对应的pid即可)
内存指针(一组)
内存指针描述了进程使用内存资源详细情况~~
进程运行过程中,是需要消耗系统的一些资源的.其中内存就是一种最重要的资源
在系统中,内存不是随便使用,都需要先申请,再分配,你才能使用
每个进程,都必须使用自己申请的内存空间.
内存指针,就是用来描述说你这个进程,都能使用哪些内存
一个进程"运行起来"是需要知道有"指令"和"数据"(指令和数据,都是需要加载到内存中的)
进程也需要知道,哪里存的是指令,哪里存的是数据.
双击一个exe,就会运行程序,加载的过程,就是系统先把exe文件里的内容(包含了指令和数据,先加载到内存当中)然后再创建进程,让进程开始执行.
文件描述符表
描述了进程所涉及到的硬盘相关的资源
操作系统,把所有盘都抽象封装成了文件
因为我们的进程需要经常访问硬盘,所以操作系统对于硬盘这样的硬件设备进行了封装=>~~
一个进程想要操作文件,就需要先打开文件,就是让你的进程在文件描述符表中分配一个表项(构建一个结构体)表示文件的相关信息
进程是系统分配资源的基本单位.(内存,硬盘->就在PCB中有所体现)
一个进程消耗CPU,我们该怎样理解
我们可以把CPU看做是一个舞台,需要执行的指令就是舞台,把进程看做是演员
进程要执行指令
同一个时刻,同一个舞台,只能有一个演员上台
也就是同一时刻,同一个核心,只能有一个进程
假如现在有一个CPU,逻辑核心有16个,但是进程繁多,那么该怎么办.
我们用到了一个非常关键的概念,分时复用(并发)
关键要点-让多个舞台,轮流上台
CPU在只有一个核心的情况下,先让进程一执行一段时间,然后让进程1下来,让进程2执行,然后过一段时间,再让进程2下来,进程3上去,周而复始,只要我切换的速度够快,那么就相当于这么多的进程是同时执行了,当然如果系统进程太多了,负担太重,就会出现卡顿
随着CPU的发展,现在有四个舞台,同时就可以有4个不同的进程,在四个不同的舞台上同时进行,此时刺客,在微观上看,这几个进程是同时执行,不是依靠快速切换模式的"同时执行"
称为"并行执行"
当然,前面的并发执行也仍然存在,每个核心都要分时复用,仍要快速切换
两个进程是并行执行,还是并发执行,是需要看系统的调度的
实际情况中,我们往往把并发和并行统称为并发
通过并发来加快IO效率
PCB中就有一些属性,来支持系统完成对这些进程的调度
1)状态
用来描述某个进程是否能够去cpu上执行,有的时候,某个进程这会不太方便,你如Scanner ,你不知道用会什么时候输入内容,这是一个不可控的事情.
就绪状态:随时准备好去上CPU,操作系统,打一声招呼就可以了.
阻塞状态:这个进程,当前不方便去CPU上执行.不应该去调度他,(比如,进程在等待IO,来自控制台的输入 输出/硬盘输入输出/网卡输入输出)
2)优先级
多个进程等待系统调度~~
多个进程的调度总有一个先后顺序
先调度谁,后调度谁,谁长谁点都是可以进行调配的
记账信息
针对每个进程,占据了多少cpu时间,进行一个统计.会根据这个统计结果来进一步的调整调度的策略~~
因此,就需要在下一个轮次进行调整.
确保每个进程都不至于出现完全捞不着CPU的问题
上下文
支撑进程调度的重要属性,是PCB中的数据结构,
存档,需要在进程调度出cpu之前,把当前寄存器的这些信息,给单独保存到一个地方
读档,在该进程下次再去cpu执行的时候,再把这些寄存器的信息给恢复回来
操作系统调度进程,过程可以认为是随机的,任何一个进程,代码执行到任何一条指令的时候,都有可能被调度出cpu,在进程下次调度回cpu的时候,继续之前的进度来执行.
所谓的"保存上下文"就是把cpu的关键寄存器中的数据,保存到内存当中(PCB的上下文属性)
所谓的恢复上下文就是把内存中的关键寄存器中的数据,加载到cpu对应的寄存器中.
内存分配-------内存管理
进程如何管理内存是一个非常复杂的事情.后续再讨论
结论:每个进程的内存,是彼此独立的,互不干扰的.
通常情况下,进程A不能直接访问进程B的内存;
目的:为了系统的稳定性,如果某个进程出现bug(比如内存写越界)出错影响的范围,只是影响到自己这个进程,不会影响到其他进程,否则,要是系统上的一个进程崩溃,影响了其他进程,这是一个非常糟糕的事情.
我们把这种情况称为"进程独立性".
进程间通信
虽然有进程的独立性,但我们有时候也需要多个进程相互配合,玩成某个工作,
进程间通信和进程的独立性并不冲突.
系统会提供一些公共的空间(多个进程都能访问到),让两个进程通过公共空间来交互数据.
java中,主要的进程间通信方式使用文件和网络.
线程.
多任务操作系统,希望系统能够同时运行多个程序~如果是单任务,不涉及到调度的问题
本质上来说,进程,是可以很好的解决并发编程这样的问题的.
在一些特定的情况下,表现的不尽人意
比如,在某些场景下,需要频繁的创建和销毁进程的时候,此时需要使用多进程编程,系统开销机会非常大.
我们实际的服务器在同一时刻会收到很多请求,对于每一个请求都会相应的创建一个进程,给这个请求提供一定的服务,返回对应的相应.一旦这个请求处理完了,此时这个进程就要销毁了.
如果请求过多,意味着你的服务器就需要不停的创建新的进程,也不停的销毁旧的进程.频繁地创建和释放.
这样进行操作,系统的开销是很大的.其中最关键的原因在于,资源的申请和释放,进程是自愿(cpu,硬盘,内存,网络带宽)分配的基本单位.
一个进程刚刚启动的时候,首当其冲就是内存资源
进程需要把依赖的代码和数据,从磁盘加载到内存中区.
从系统分配一个内存,不是一件容易的事情.
一般来说,申请内存的时候,需要指定一个大小,系统内部就会把各种大小的空闲内存,通过一定的数据结构,给组织起来了.实际申请的时候,就需要区这样的空间中去进行查找,找到个合适的空闲内存,分配过来.
结论:
进程在进行频繁创建的销毁的时候,开销比较大(开销主要体现在资源的申请和释放上)
线程:
线程也可以称为"轻量级进程",在进程的基础上,做出了改进
保持了独立调度执行,这样的"并发支持",同时省去了"分配资源""释放资源"带来的额外开销.
这个图上,是一个进程只有一个PCB,但实际上,一个进程可以有多个PCB,意味着一个线程包含了一个线程组,包含了多个线程,
操作系统,进行"多任务调度",本质上是在调度PCB~~(线程在系统中的调度规则,就和之前的线程是一样的)
多个线程的PCB的内存指针,指向的是同一个内存空间.
这就意味着,只是创建第一个线程的时候,需要从系统分配资源.后续的线程,就不必再分配,直接共用前面的那份资源就可以了.除了内存之外,文件描述符表(操作硬盘),这个东西也是多个进程共用一份的.
能够共享资源的这些线程,分成组,称为线程组,线程组也是线程的一部分.
线程组指向的内存空间包含了所有线程依赖的所有数据和代码,这些线程啃是各取所需,也可能是有一定公共的~~
数据也是共享的关系,意味着创建第一个线程之后,申请这些资源,后续的就不必去申请了,
线程解决的问题,是能够降低频繁申请释放带来的开销的.
进程是自愿分配的基本单位,线程是调度执行的基本单位.
创建进程,资源就分配了,只不过,一个进程中至少包含一个线程~~(创建第一个线程的同时,进程也就被创建出来了).
加快效率:
多进程的解决方式:
创建新的进程,就需要申请更多的资源
多线程的解决方式:
引入更多的线程,速度变快,共享一份资源,资源开销更少了.
当引入的线程达到一定数量之后,再尝试引入新的进程之后,效率就很难再次提升了.
当线程数量过多的时候,线程之间会相互竞争,cpu的资源核心是有限的,非但不会提高效率,反而会增加调度的开销.
多线程之间可能会起冲突,就会导致代码中出现一些逻辑上的错误.(线程安全问题)
副作用:
多线程:一个线程如果抛出异常,并且没有处理好,就可能会导致整个进程被终止.
进程与线程的关系/区别
小结:
1.进程是包含线程的
2.每个线程,也是一个独立的执行流,可以执行一些代码,并且单独的参与到cpu的调度上(状态,上下文,优先级,记账信息.每个线程都有自己单独的一份)
3.每个进程都有自己的资源,进程中的线程共用这一份资源(文件描述符表和内存空间)
进程是资源分配的基本单位,线程是调度执行的基本单位.
4.进程和进程之间,不会相互影响,如果同一个进程中的线程,抛出异常,是可能会影响到其他线程的,会把整个进程中的所有线程都异常终止.
5.同一个进程中的线程之间,可能会相互干扰,引发线程问题/
6.线程也不是越多越好,要能够合适,如果线程太多了,调度开销可能就会非常明显.
线程创建
class MyTread extends Thread{
public void run()
{
}
}
这里我们创建出的线程,需要继承Thread.
这里的run方法,相当于卖你方法,一个java程序(进程)执行的入口方法,是一样的道理
一个进程至少有一个线程,这个进程中的第一个线程,称为主线程,main方法,就是主线程的入口方法.
此处的run方法,不需要程序员进行手动调用,会在合适的时机(线程创建好了之后),被jvm自动调用执行.
//MyThread t=new MyThread();
Thread t=new MyThread();
//这两个写法都可以
//调用thread的start方法,才会真正调用系统的api,在系统内核中创建出线程.
t.start();