配色: 字号:
协程 进程
2021-09-03 | 阅:  转:  |  分享 
  
协程、进程与线程笔记整理进程与线程进程(process)是一个执行中的程序的实例,操作系统对一个正在运行的程序的一种抽象。运从内核的观点看,
进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。行中的程序,就被称为「进程」。线程(Thread):线程是CPU调
度的最小单位(程序执行流的最小单元),它被包含在进程之中,是进程中的实际运作单元。线程是进程的一个实体。线程是进程中一个单一顺序的
控制流,一个进程可以由多个线程的执行单元组成,每个线程都运行在进程的上下文中,并共享全局数据。一个标准的线程有线程ID、当前指令指
针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单元,线程自己不拥有系统资源,只拥有一
点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。一个线程可以创建和撤销另一个线程,同一进程
中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现处间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状
态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如
某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。进程是资源的分配单位,而线程是运行调
度单位。进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。并发与并行程序是死的(静态的),进程是
活的(动态的)。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统
本身;所有由用户启动的进程都是用户进程。多进程:在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是
多任务。多线程:在单一程序中同时运行多个想成完成不同的工作?并发运行,指的是一个进程的指令和另一个进程的指令交错执行。操作系统实现
这种交错执行的机制称为上下文切换。协程的本质就是函数、内核(Kernel):一个计算机程序,用来管理软件发出的数据I/O(输入与输
出)要求,将这些要求转译为数据处理的指令,交由中央处理器(CPU)及计算机中其他电子组件进行处理,是现代操作系统中最基本的部分。外
壳(Shell):指“为使用者提供使用者界面”的软件,通常指的是命令行界面的解析器。一般来说,这个词是指操作系统中提供存取内核所提
供之服务的程式。Shell也用于泛指所有为用户提供操作界面的程序,也就是程序和用户交互的层面。内核不提供交互。抢占(Preempt
ion):分为非抢占式和抢占式。根据调度主体分用户抢占与内核抢占。非抢占式(Nonpreemptive):让进程运行直到结束或阻塞
的调度方式。抢占式(Preemptive):将逻辑上可继续运行的在运行过程暂停的调度方式。可防止单一进程长时间独占CPU。异常控制
流(ECF,ExceptionalControlFlow)ECF发生在硬件层,操作系统层,应用层。控制转移(controlt
ransfer)是指程序计数器对应的指令序列的跳转,控制转移序列的叫做处理器的控制流(controlflow)。某些如跳转、调用
和返回是为了使得程序对内部状态变化(event)做出反应而设计的机制,系统通过使控制流发生突变对发生各种状态变化。Exceptio
ns:任何情况下,处理器检测到event发生,通过异常表(exceptiontable)跳转到专门处理这类事件的操作系统子程序(
exceptionhandler)。异步异常由事件产生,同步异常是执行一条指令的直接产物。类别包含中断(异步),陷阱(同步),故
障(同步),终止(同步)。中断:异步发生,处理器IO设备信号的结果。陷阱:有意的异常。最重要的用途是在用户程序和内核之间提供一个像
过程一样的接口,叫做系统调用。故障:潜在可恢复的错误造成的结果。如果能被修复,则重新执行引起故障的指令,否则终止。终止:不可恢复的
致命错误造成的结果。有高达256种不同的异常类型,如出发错误(0)、一般保护故障(13)、缺页(14)、机器检查(18)、操作系统
定义的异常(32-127,129-255)、系统调用(0x80)。逻辑控制流(LogicalControlFlow):程序计数
器PC值的序列叫做逻辑控制流(逻辑流)。PC对应于程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令
。逻辑流看起来就像是在独占处理器地执行程序,每个进程执行逻辑流的一部分然后就被抢占,实际上处理器通过上下文保护好进程间的信息,在不
同的进程中切换。并发流(ConcurrentFlows):并发流指逻辑流在执行时间上与另一个流重叠,多个就叫并发(concurr
ent)。一个进程和其他进程轮流运行叫多任务(multitasking)。多任务也叫做时间分片(timeslicing)。进程占
有CPU执行控制流的每一个时间段叫时间片(timeslice)。如果两个流并发运行在不同的处理器或者计算机,称为并行流(para
llelflow)。线程进程的关系一个线程只能属于一个进程,而一个进程可以有多个线程,至少有一个线程;资源分配给进程,同一进程内
的所有线程共享该进程的所有资源;线程在执行过程中需要协作同步。不同进程中的线程之间要利用消息通信的方法实现同步;处理机分配给线程,
即真正在处理机上运行的是线程;线程是进程的一个执行单元,也是进程内的可调用实体。线程进程的区别线程共享内存空间;进程的内存是独立的
;进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的系统资源;调度:线程作为分配和调度的基本单位,进程作为
拥有资源的基本单位线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;同一个进程的线程之间可以直接交流;两个进程想通信,必须
通过一个中间代理来实现;进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通
信机制很方便。创建新进程很简单;创建新进程需要对其父进程进行一个克隆;一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作
子进程;改变线程(如优先权),可能会影响其他线程;改变父进程,不影响子进程。并发性:不同进程之间可以并发执行,同一进程内的线程也可
以并发执行系统开销:在创建和撤销进程的时候,系统都要分配和回收资源,系统开销大。但进程有独立的地址空间,进程崩溃后,在保护模式的下
不会对其他进程造成影响。线程有自己的堆栈和局部变量,没有独立的地址空间,一个线程死后就等于整个进程死掉,安全性差。总结:多线程执行
效率高;多进程耗资源,安全。进程的优缺点优点顺序程序的特点:具有封闭性和可再现性;程序的并发执行和资源共享。多道程序设计出现后
,实现了程序的并发执行和资源共享,提高了系统的效率和系统的资源利用率。线程的优缺点使多核CPU系统更加有效。操作系统会保证当线程数
不大于核数目时,不同的线程运行于不同的核上;改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分
,这样的程序会利于理解和修改。调度时,要保存线程状态,频繁调度,需要占用大量的机时;程序设计上容易出错(线程同步问题)。多线程的
优缺点优点1)无需跨进程边界;程序逻辑和控制方式简单;2)所有线程可以直接共享内存和变量等;3)线程方式消耗的总资源比进程方式好。
?缺点每个线程与主程序共用地址空间,受限于2GB地址空间;线程之间的同步和加锁控制比较麻烦;一个线程的崩溃可能影响到整个程序的稳定
性;到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如WindowsServer2003,大约是1500个左右的线程
数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;线程能够提高的总性能有限,而且线程多了之
后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。多进程的优缺点优点每个进程互相独立,不影响主程序的稳定性,子进程崩溃没
关系;通过增加CPU,就可以容易扩充性能;可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;
每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。?缺点逻辑控制复杂,需要和主程序交互;需要跨进程边界,如果有
大数据量传送,就不太好,适合小数据量传送、密集运算多进程调度开销比较大。对比类别多进程多线程总结数据共享、同步数据共享复杂,需要
用IPC;数据是分开的,同步简单?因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂各有优势内存、CPU占用内存多,切
换复杂,CPU利用率低占用内存少,切换简单,CPU利用率高线程占优创建销毁、切换创建销毁、切换复杂,速度慢创建销毁、切换简单,速度
很快线程占优编程、调试编程简单,调试简单编程复杂,调试复杂进程占优可靠性进程间不会互相影响一个线程挂掉将导致整个进程挂掉进程占优分
布式适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单适应于多核分布式进程占优进程与线程的选择需要频繁创建销毁的优先
使用线程;需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应强相关处理用线程、弱相关处理用进程。多机分布
的用进程,多核分布用线程;需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。进程运行分析私有地址空间(PrivateAd
dressSpace)一般,进程间地址空间读写保护。进程地址空间32位进程,代码段从0x08048000开始,64位进程从0x0
0400000开始:用户模式和内核模式(UserandKernelModes):通过控制寄存器中的模式位(modebit)
描述进程当前享有的特权。内核模式:(超级用户)可执行指令集中任何指令,并且可以访问系统中任何存储器位置。用户模式:不允许执行特权指
令,不允许直接引用地址空间中内核区内的代码和数据,任何尝试都会引发致命保护故障。可以通过系统调用接口间接访问内核代码和数据。上下文
切换(ContextSwitches):内核为每个进程维持一个上下文(context),是内核重新启动一个被抢占的进程所需的状态
。包括:通用目的的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(地址空间的页表、有关当前进程信息的
进程表、进程已打开文件的信息的文件表)内核调度器(scheduler)负责调度进程,抢占当前进程,重新开始先前被抢占的进程。进程和
线程基础知识什么是进程?标准定义:进程是一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行程序的实例
,包括程序计数器、寄存器和程序变量的当前值。简单来说进程就是一个程序的执行流程,内部保存程序运行所需的资源。在操作系统中可以有多个
进程在运行,可对于CPU来说,同一时刻,一个CPU只能运行一个进程,但在某一时间段内,CPU将这一时间段拆分成更短的时间片,CPU
不停的在各个进程间游走,这就给人一种并行的错觉,像CPU可以同时运行多个进程一样,这就是伪并行。进程和程序有什么联系?一个进程是某
种类型的一个活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而
为另一个进程提供服务。程序是产生进程的基础程序的每次运行产生不同的进程进程是程序功能的体现通过多次执行,一个程序可对应多个进程;通
过调用关系,一个进程可包括多个程序进程和程序有什么区别?进程是动态的,程序是静态的:程序是有序代码的集合,进程是程序的执行。进程是
暂时的,程序是永久的:进程是一个状态变化的过程,程序可长久保存。进程和程序的组成不同:进程的组成包括程序、数据和进程控制块(进程状
态信息)。进程有什么特点?动态性:可动态的创建和结束进程并发性:可以被独立的调度并占用处理机并发运行独立性:不同进程的工作不相互影
响制约性:因访问共享资源或进程间同步而产生制约进程如何创建?有什么事件会触发进程的创建呢?系统初始化:当启动操作系统时,通常会创建
很多进程,有些是同用户交互并替他们完成工作的前台进程,其它的都是后台进程,后台进程和特定用户没有关系,但也提供某些专门的功能,例如
接收邮件等,这种功能的进程也称为守护进程。计划任务是个典型的守护进程,它每分钟运行一次来检查是否有工作需要它完成。如果有工作要做,
它就会完成此工作,然后进入休眠状态,直到下一次检查时刻的到来。正在运行的程序执行了创建进程的系统调用:在一个进程中又创建了一个新的
进程,这种情况很常见。用户请求创建一个新进程:这种情况相信每个人都见过,用电脑时双击某个应用图标,就会有至少一个进程被创建。一个批
处理作业的初始化:这种情形不常见,仅在大型机的批处理系统中应用,用户在这种系统中提交批处理作业,在操作系统认为有资源可运行另一个作
业时,它创建一个新的进程,并运行其输入队列中的下一个作业。归根到底:在UNIX系统中,只有fork系统调用才可以创建新进程,使用方
式如下:#include#includeintmain(){?pid_tid=
fork();?if(id<0){?perror(''fork\n'');?}elseif(id==0){//
子进程?printf(''子进程\n'');?}else{//父进程?printf(''父进程\n'');?}?return0;
}进程创建之后,父子进程都有各自不同的地址空间,其中一个进程在其地址空间的修改对另一个进程不可见。子进程的初始化空间是父进程的一个
副本,这里涉及两个不同地址空间,不可写的内存区是共享的,某些UNIX的实现使程序正文在两者间共享,因为它是不可修改的。还有一种写时
复制共享技术,子进程共享父进程的所有内存,一旦两者之一想要修改部分内存,则这块内存被复制确保修改发生在当前进程的私有内存区域。进程
如何终止?有什么事件会触发进程的终止呢?正常退出(自愿):进程完成了工作正常终止,UNIX中退出进程的系统调用是exit。出错退出
(自愿):进程发现了错误而退出。可以看如下代码:#include#includevoid
Func(){if(error){//有错误就退出程序exit(1);}}intma
in(){Func();}严重错误(非自愿):进程发生了严重的错误而不得不退出,通常是程序的错误导致,例如执行了一条非法
指令,引用不存在的内存,或者除数是0等,出现这些错误时进程默认会退出。而有些时候如果用户想自行处理某种类型的错误,发生不同类型错误
时进程会收到不同类型的信号,用户注册处理不同信号的函数即可。被其它进程杀死(非自愿):其它进程执行kill系统调用通知操作系统杀死
某个进程。操作系统如何进行进程管理?这里就不得不提到一个数据结构:进程控制块(PCB),操作系统为每个进程都维护一个PCB,用来保
存与该进程有关的各种状态信息。进程可以抽象理解为就是一个PCB,PCB是进程存在的唯一标志,操作系统用PCB来描述进程的基本情况以
及运行变化的过程,进程的任何状态变化都会通过PCB来体现。PCB包含进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所
打开文件的状态、账号和调度信息,以及其它在进程由运行态转换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未
中断过一样。后一小节会具体介绍PCB。提到进程管理,有一个概念我们必须要知道,就是中断向量,中断向量是指中断服务程序的入口地址。一
个进程在执行过程中可能会被中断无数次,但是每次中断后,被中断的进程都要返回到与中断发生前完全相同的状态。中断发生后操作系统最底层做
了什么呢?1)硬件压入堆栈程序计数器等;2)硬件从中断向量装入新的程序计数器;3)汇编语言过程保存寄存器值;4)汇编语言过程设置新
的堆栈;5)C中断服务例程运行(典型的读和缓冲输入);6)调度程序决定下一个将运行的进程;7)C过程返回到汇编代码;8)汇编语言过
程开始运行新的当前进程。进程控制块中存储了什么信息?进程标识信息:如本进程的标识,本进程的父进程标识,用户标识等。处理机状态信息保
护区:用于保存进程的运行现场信息:用户可见寄存器:用户程序可以使用的数据,地址等寄存器控制和状态寄存器:程序计数器,程序状态字栈指
针:过程调用、系统调用、中断处理和返回时需要用到它进程控制信息:调度和状态信息:用于操作系统调度进程使用进程间通信信息:为支持进程
间与通信相关的各种标识、信号、信件等,这些信息存在接收方的进程控制块中存储管理信息:包含有指向本进程映像存储空间的数据结构进程所用
资源:说明由进程打开使用的系统资源,如打开的文件等有关数据结构连接信息:进程可以连接到一个进程队列中,或连接到相关的其他进程的PC
B进程如何进行生命周期管理?进程创建:创建进程有三个主要事件:系统初始化用户请求创建一个新进程一个正在运行的进程执行创建进程的系统
调用进程运行:内核选择一个就绪的进程,让它占用处理机并运行,这里就涉及到了进程的调度策略,选择哪个进程调度?为什么选择调度这个进程
呢?(莫慌,下面会介绍哈)进程等待:在以下情况下进程会等待(阻塞):请求并等待系统服务,无法马上完成启动某种操作,无法马上完成需要
的数据没有到达。注意:进程只能自己阻塞自己,因为只有进程自身才能知道何时需要等待某种事件的发生。进程唤醒:进程只能被别的进程或操作
系统唤醒,唤醒进程的原因有:被阻塞进程需要的资源可被满足被阻塞进程等待的事件到达将该进程的PCB插入到就绪队列进程结束:在以下四种
情况下进程会结束:自愿型正常退出自愿型错误退出强制型致命错误退出强制型被其它进程杀死退出进程都有什么状态?不同系统设置的进程状态是
不同的,多数系统中的进程在生命结束前有三种基本状态,进程只会处于三种基本状态之一:运行状态:进程正在处理机上运行时就处在运行状态,
该时刻进程时钟占用着CPU;就绪状态:万事俱备,只欠东风,进程已经获得了除处理机之外的一切所需资源,一旦得到处理机就可以运行;就绪
态中的进程其实可以运行,但因为其它进程正在占用着CPU而暂时停止运行;等待状态(阻塞状态):进程正在等待某一事件而暂停运行,等待某
个资源或者等待输入输出完成。除非某种外部事件发生,否则阻塞态的进程不能运行;进程状态变化图如下:在操作系统发现进程不能继续运行下去
时,进程因为等待输入而被阻塞,进程从运行态转换到阻塞态!调度程序选择了另一个进程执行时,当前程序就会从运行态转换到就绪态!被调度程
序选择的程序会从就绪态转换到运行态!当阻塞态的进程等待的一个外部事件发生时,就会从阻塞态转换到就绪态,此时如果没有其他进程运行时,
则立刻从就绪态转换到运行态!某些系统设置下进程还会有其它状态:创建状态:进程正在被创建还没被转到就绪状态之前的状态;结束状态:进程
正在从系统中消失时的状态。有些与进程管理相关的系统调用读者有必要了解一下:pid=fork();//创建一个与父进程一样的子进
程pid=waitpid();//等待子进程终止s=execve();//替换进程的核心映像exit();//终止进程
运行并返回状态值s=sigaction();//定义信号处理的动作s=sigprocmask();//检查或更换信号掩码s
=sigpending();//获得阻塞信号集合s=sigsuspend();//替换信号掩码或挂起进程alarm();
//设置定时器pause();//挂起调用程序直到下一个信号出现什么是进程挂起?为什么会出现进程挂起?进程挂起就是为了合理且
充分的利用系统资源,把一个进程从内存转到外存。进程在挂起状态时,意味着进程没有占用内存空间,处在挂起状态的进程映射在磁盘上。进程挂
起通常有两种状态:阻塞挂起状态:进程在外存并等待某事件的出现;就绪挂起状态:进程在外存,但只要进入内存即可运行。有什么与进程挂起相
关的状态转换?进程挂起可能有以下几种情况:阻塞到阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时,会进行这种转换,以提交
新进程或运行就绪进程;就绪到就绪挂起:当有高优先级阻塞进程或低优先级就绪进程时,系统会选择挂起低优先级就绪进程;运行到就绪挂起:对
于抢占式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转到就绪挂起状态;阻塞挂起到就绪挂起:当
有阻塞挂起进程有相关事件出现时,系统会把阻塞挂起进程转换为就绪挂起进程。有进程挂起那就有进程解挂:指一个进程从外存转到内存,相关状
态有:就绪挂起到就绪:没有就绪进程或就绪挂起进程优先级高于就绪进程时,就会进行这种转换;阻塞挂起到阻塞:当一个进程释放足够内存时,
系统会把一个高优先级阻塞挂起进程转换为阻塞进程。什么是进程调度?操作系统对于进程调度都有什么策略?当系统中有多个进程同时竞争CPU
,如果只有一个CPU可用,那同一时刻只会有一个进程处于运行状态,操作系统必须要选择下一个要运行的是哪个进程,在操作系统中,完成选择
工作的这部分称为调度程序,该程序使用的算法称作调度算法。什么时候进行调度?系统调用创建一个新进程后,需要决定是运行父进程还是运行子
进程一个进程退出时需要做出调度决策,需要决定下一个运行的是哪个进程当一个进程阻塞在I/O和信号量或者由于其它原因阻塞时,必须选择另
一个进程运行当一个I/O中断发生时,如果中断来自IO设备,而该设备现在完成了工作,某些被阻塞的等待该IO的进程就成为可运行的就绪进
程了,是否让新就绪的进程运行,或者让中断发生时运行的进程继续运行,或者让某个其它进程运行,这就取决于调度程序的抉择了。调度算法可以
分类:非抢占式调度算法:挑选一个进程,然后让该进程运行直至被阻塞,或者直到该进程自动释放CPU,即使该进程运行了若干个小时,它也不
会被强迫挂起。这样做的结果是,在时钟中断发生时不会进行调度,在处理完时钟中断后,如果没有更高优先级的进程等待,则被中断的进程会继续
执行。简单来说,调度程序必须等待事件结束。非抢占方式引起进程调度的条件:进程执行结束,或发生某个事件而不能继续执行正在运行的进程因
有I/O请求而暂停执行进程通信或同步过程中执行了某些原语操作(wait、block等)抢占式调度算法:挑选一个进程,并且让该进程运
行某个固定时段的最大值。如果在该时段结束时,该进程仍在运行,它就被挂起,而调度程序挑选另一个进程运行,进行抢占式调度处理,需要在时
间间隔的末端发生时钟中断,以便CPU控制返回给调度程序,如果没有可用的时钟,那么非抢占式调度就是唯一的选择。简单来说,就是当前运行
的进程在事件没结束时就可以被换出,防止单一进程长时间独占CPU资源。下面会介绍很多抢占式调度算法:优先级算法、短作业优先算法、轮转
算法等。调度策略:不同系统环境下有不同的调度策略算法。调度算法也是有KPI的,对调度算法首先提的需求就是:公平:调度算法需要给每个
进程公平的CPU份额,相似的进程应该得到相似的服务,对一个进程给予较其它等价的进程更多的CPU时间是不公平的,被普通水平的应届生工
资倒挂也是不公平的!执行力:每一个策略必须强制执行,需要保证规定的策略一定要被执行。平衡:需要保证系统的所有部分尽可能都忙碌但是因
为不同的应用有不同的目标,不同的系统中,调度程序的优化也是不同的,大体可以分为三种环境:批处理系统批处理系统的管理者为了掌握系统的
工作状态,主要关注三个指标:吞吐量:是系统每小时完成的作业数量周转时间:指从一个作业提交到完成的平均时间CPU利用率:尽可能让CP
U忙碌,但又不能过量调度算法:先来先服务先来后到嘛,就像平时去商店买东西需要排队一样,使用该算法,进程按照它们请求CPU的顺序来使
用CPU,该算法最大的优点就是简单易于实现,太容易的不一定是好的,该算法也有很大的缺点:平均等待时间波动较大,时间短的任务可能排队
排在了时间长的任务后面。举个生活中的例子,排着队去取快递,如果每个人都很快取出来快递还好,如果前面有几个人磨磨唧唧到快递柜前才拿出
手机打开app,再找半分钟它的取件码,就会严重拖慢后面的人取快递的速度,同理排着队的进程如果每个进程都很快就运行完还好,如果其中有
一个得到了CPU的进程运行时候磨磨唧唧很长时间都运行不完,那后面的进程基本上就没有机会运行了!最短作业优先该调度算法是非抢占式的算
法,每个进程执行期间不会被打断,每次都选择执行时间最短的进程来调度,但问题来了,操作系统怎么可能知道进程具体的执行时间呢,所以该算
法注定是基于预测性质的理想化算法,而且有违公平性,而且可能导致运行时间长的任务得不到调度。最短剩余时间优先该调度算法是抢占式的算法
,是最短作业优先的抢占版本,在进程运行期间,如果来了个更短时间的进程,那就转而去把CPU时间调度给这个更短时间的进程,它的缺点和最
短作业优先算法类似。交互式系统对于交互系统最重要的指标就是响应时间和均衡性啦:响应时间:一个请求被提交到产生第一次响应所花费的时间
。你给别人发微信别人看后不回复你或者几个小时后才回复你,你是什么感受,这还是交互式吗?均衡性:减少平均响应时间的波动。需要符合固有
期望和预期,你给别人发微信,他有时候秒回复,有时候几个小时后才回复。在交互式系统中,可预测性比高差异低平均更重要。调度算法:轮转调
度每个进程被分配一个时间段,称为时间片,即CPU做到雨露均沾,轮流翻各个进程的牌子,这段时间宠幸进程A,下一段时间宠幸进程B,再下
一段时间宠幸进程C,确保每个进程都可以获得CPU时间,如果CPU时间特别短的话,在外部看来像是同时宠幸了所有进程一样。那么问题来了
,这个时间片究竟多长时间好呢?如果时间片设的太短会导致过多的进程切换,频繁的上下文切换会降低CPU效率,而如果时间片设的太长又可能
对短的交互请求的响应时间变长,通常将时间片设为20-50ms是个比较合理的折中,大佬们的经验规则时维持上下文切换的开销处于1%以内
。优先级调度上面的轮转调度算法是默认每个进程都同等重要,都有相同优先级,然而有时候进程需要设置优先级,例如某些播放视频的前台进程可
以优先于某些收发邮件的后台守护进程被调度,在优先级调度算法中,每个优先级都有相应的队列,队列里面装着对应优先级的进程,首先在高优先
级队列中进行轮转调度,当高优先级队列为空时,转而去低优先级队列中进行轮转调度,如果高优先级队列始终不为空,那么低优先级的进程很可能
就会饥饿到很久不能被调度。多级队列多级队列算法与优先级调度算法不同,优先级算法中每个进程分配的是相同的时间片,而在多级队列算法中,
不同队列中的进程分配给不同的时间片,当一个进程用完分配的时间片后就移动到下一个队列中,这样可以更好的避免上下文频繁切换。举例:有一
个进程需要100个时间片,如果每次调度都给分配一个时间片,则需要100次上下文切换,这样CPU运行效率较低,通过多级队列算法,可以
考虑最开始给这个进程分配1个时间片,然后被换出,下次分给它2个时间片,再换出,之后分给它4、8、16、64个时间片,这样分配的话,
该进程只需要7次交换就可以运行完成,相比100次上下文切换运行效率高了不少,但顾此就会失彼,那些需要交互的进程得到响应的速度就会下
降。最短进程优先交互式系统中应用最短进程优先算法其实是非常适合的,每次都选择执行时间最短的进程进行调度,这样可以使任务的响应时间最
短,但这里有个任务,还没有运行呢,我怎么知道进程的运行时间呢?根本没办法非常准确的再当前可运行进程中找出最短的那个进程。有一种办法
就是根据进程过去的行为进行预测,但这能证明是个好办法吗?保证调度这种调度算法就是向用户做出明确的可行的性能保证,然后去实现它。一种
很实际的可实现的保证就是确保N个用户中每个用户都获得CPU处理能力的1/N,类似的,保证N个进程中每个进程都获得1/N的CPU时间
。彩票调度彩票调度算法基本思想是为进程提供各种资源(CPU时间)的彩票,一旦需要做出调度决策时,就随机抽出一张彩票,拥有该彩票的进
程获得该资源,很明显,拥有彩票越多的进程,获得资源的可能性越大。该算法在程序喵看来可以理解为股票算法,将CPU的使用权分成若干股,
假设共100股分给了3个进程,给这些进程分别分配20、30、50股,那么它们大体上会按照股权比例(20:30:50)划分CPU的使
用。公平分享调度假设有系统两个用户,用户1启动了1个进程,用户2启动了9个进程,如果使用轮转调度算法,那么用户1将获得10%的CP
U时间,用户2将获得90%的CPU时间,这对用户来说公平吗?如果给每个用户分配50%的CPU时间,那么用户2中的进程获得的CPU时
间明显比用户1中的进程短,这对进程来说公平吗?这就取决于怎么定义公平啦?实时系统实时系统顾名思义,最关键的指标当然是实时啦:满足截
止时间:需要在规定deadline前完成作业;可预测性:可预测性是指在系统运行的任何时刻,在任何情况下,实时系统的资源调配策略都能
为争夺资源的任务合理的分配资源,使每个实时任务都能得到满足。调度算法分类:硬实时必须在deadline之前完成工作,如果delay
,可能会发生灾难性或发生严重的后果;软实时必须在deadline之前完成工作,但如果偶尔delay了,也可以容忍。调度算法:单调速
率调度采用抢占式、静态优先级的策略,调度周期性任务。每个任务最开始都被配置好了优先级,当较低优先级的进程正在运行并且有较高优先级的
进程可以运行时,较高优先级的进程将会抢占低优先级的进程。在进入系统时,每个周期性任务都会分配一个优先级,周期越短,优先级越高。这种
策略的理由是:更频繁的需要CPU的任务应该被分配更高的优先级。最早截止时间调度根据截止时间动态分配优先级,截止时间越早的进程优先级
越高。该算法中,当一个进程可以运行时,它应该向操作系统通知截止时间,根据截止时间的早晚,系统会为该进程调整优先级,以便满足可运行进
程的截止时间要求。它与单调速率调度算法的区别就是一个是静态优先级,一个是动态优先级。如何配置调度策略?调度算法有很多种,各有优缺点
,操作系统自己很少能做出最优的选择,那么可以把选择权交给用户,由用户根据实际情况来选择适合的调度算法,这就叫策略与机制分离,调度机
制位于内核,调度策略由用户进程决定,将调度算法以某种形式参数化,由用户进程来选择参数从而决定内核使用哪种调度算法。操作系统怎么完成
进程调度?进程的每次变化都会有相应的状态,而操作系统维护了一组状态队列,表示系统中所有进程的当前状态;不同的状态有不同的队列,有就
绪队列阻塞队列等,每个进程的PCB都根据它的状态加入到相应的队列中,当一个进程的状态发生变化时,它的PCB会从一个状态队列中脱离出
来加入到另一个状态队列。注意图中同一种状态为什么有多个队列呢?因为进程有优先级概念,相同状态的不同队列的优先级不同。什么是线程?线
程是进程当中的一条执行流程,这几乎就是进程的定义,一个进程内可以有多个子执行流程,即线程。可以从两个方面重新理解进程:从资源组合的
角度:进程把一组相关的资源组合起来,构成一个资源平台环境,包括地址空间(代码段、数据段),打开的文件等各种资源从运行的角度:代码在
这个资源平台上的执行流程,然而线程貌似也是这样,但是进程比线程多了资源内容列表样式:那就有一个公式:进程=线程+共享资源为
什么使用线程?因为要并发编程,在许多情形中同时发生着许多活动,而某些活动有时候会被阻塞,通过将这些活动分解成可以准并行运行的多个顺
序流程是必须的,而如果使用多进程方式进行并发编程,进程间的通信也很复杂,并且维护进程的系统开销较大:创建进程时分配资源建立PCB,
撤销进程时回收资源撤销PCB,进程切换时保存当前进程的状态信息。所以为了使并发编程的开销尽量小,所以引入多线程编程,可以并发执行也
可以共享相同的地址空间。并行实体拥有共享同一地址空间和所有可用数据的能力,这是多进程模型所不具备的能力。使用线程有如下优点:可以多
个线程存在于同一个进程中各个线程之间可以并发的执行各个线程之间可以共享地址空间和文件等资源线程比进程更轻量级,创建线程撤销线程比创
建撤销进程要快的多,在许多系统中,创建一个线程速度是创建一个进程速度的10-100倍。如果多个线程是CPU密集型的,并不能很好的获
得更好的性能,但如果多个线程是IO密集型的,线程存在着大量的计算和大量的IO处理,有多个线程允许这些活动彼此重叠进行,从而会加快整
体程序的执行速度。但也有缺点:一旦一个线程崩溃,会导致其所属进程的所有线程崩溃。由于各个线程共享相同的地址空间,那么读写数据可能会
导致竞争关系,因此对同一块数据的读写需要采取某些同步机制来避免线程不安全问题。什么时候用进程、线程?进程是资源分配单位,线程是CP
U调度单位;进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈;线程同样具有就绪阻塞和执行三种基本状态,同样具有
状态之间的转换关系;线程能减少并发执行的时间和空间开销:线程的创建时间比进程短线程的终止时间比进程短同一进程内的线程切换时间比进程
短由于同一进程的各线程间共享内存和文件资源,可直接进行不通过内核的通信结论:可以在强调性能时候使用线程,如果追求更好的容错性可以考
虑使用多进程,google浏览器据说就是用的多进程编程。在多CPU系统中,多线程是有益的,在这样的系统中,通常情况下可以做到真正的
并行。C/C++中如何使用多线程编程?POSIX使用如下线程封装函数来操作线程:pthread_create
创建一个新线程pthread_exit结束调用的线程pthread_join
等待一个特定的线程退出pthread_yield释放CPU来运行另外一个线程
pthread_attr_init创建并初始化一个线程的属性结构pthread_attr_destroy
删除一个线程的属性结构后两个函数是有关线程属性的调用。pthread_attr_init建立关联一个线程的属性结构
并初始化成默认值,这些值(优先级等)可以通过修改属性结构中的对应值来改变;pthread_attr_destroy会删除一个线程的
属性结构,释放它占用的内存,它不会影响调用它的线程,线程依然会继续存在。C++中有std::thread和async,可以很方便的
操作多线程,示例代码如下:voidF(){?cout<<''a''<readr(F);?r.detach();std::this_thread::sleep_for(std::chrono::se
conds(20));?return0;}线程是如何实现的?线程的实现可分为用户线程和内核线程:用户线程:在用户空间实现的线程机
制,它不依赖于操作系统的内核,由一组用户级的线程库函数来完成线程的管理,包括进程的创建终止同步和调度等。用户线程有如下优点:由于用
户线程的维护由相应进程来完成(通过线程库函数),不需要操作系统内核了解内核了解用户线程的存在,可用于不支持线程技术的多进程操作系统
。每个进程都需要它自己私有的线程控制块列表,用来跟踪记录它的各个线程的状态信息(PC,栈指针,寄存器),TCB由线程库函数来维护;
用户线程的切换也是由线程库函数来完成,无需用户态/核心态切换,所以速度特别快;允许每个进程拥有自定义的线程调度算法;但用户线程也有
缺点:阻塞性的系统调用如何实现?如果一个线程发起系统调用而阻塞,则整个进程在等待。当一个线程开始运行后,除非它主动交出CPU的使用
权,否则它所在进程当中的其它线程将无法运行;由于时间片分配给进程,与其它进程比,在多线程执行时,每个线程得到的时间片较少,执行会较
慢内核线程:是指在操作系统的内核中实现的一种线程机制,由操作系统的内核来完成线程的创建终止和管理。特点:在支持内核线程的操作系统中
,由内核来维护进程和线程的上下文信息(PCBTCB);线程的创建终止和切换都是通过系统调用内核函数的方式来进行,由内核来完成,因
此系统开销较大;在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其它内核线程的运行;时间片分配给线程,多线程的进程
获得更多CPU时间;tips由于在内核中创建或撤销线程的代价比较大,某些系统采取复用的方式回收线程,当某个线程被撤销时,就把它标记
不可运行,但是内核数据结构没有受到任何影响,如果后续又需要创建一个新线程时,就重新启动被标记为不可运行的旧线程,从而节省一些开销。
注意尽管使用内核线程可以解决很多问题,但还有些问题,例如:当一个多线程的进程创建一个新的进程时会发生什么?新进程是拥有与原进程相同
数量的线程还是只有一个线程?在很多情况下,最好的选择取决于进程计划下一步做什么?如果它要调用exec启动一个新程序,或许一个线程正
合适,但如果它继续运行,那么最好复制所有的线程。轻量级进程:它是内核支持的用户线程模型,一个进程可以有多个轻量级进程,每个轻量级进
程由一个单独的内核线程来支持。在Linux下是没有真正的线程的,它所谓的线程其实就是使用进程来实现的,就是所谓的轻量级进程,其实就
是进程,都是通过clone接口调用创建的,只不过两者传递的参数不同,通过参数决定子进程和父进程共享的资源种类和数量,进而有了普通进
程和轻量级进程的区别。什么是上下文切换?上下文切换指的是操作系统停止当前运行进程(从运行状态改变成其它状态)并且调度其它进程(就绪
态转变成运行状态)。操作系统必须在切换之前存储许多部分的进程上下文,必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过,同时切
换上下文这个过程必须快速,因为上下文切换操作是非常频繁的。那上下文指的是什么呢?指的是任务所有共享资源的工作现场,每一个共享资源都
有一个工作现场,包括用于处理函数调用、局部变量分配以及工作现场保护的栈顶指针,和用于指令执行等功能的各种寄存器。注意这里所说的进程
切换导致上下文切换其实不太准确,准确的说应该是任务的切换导致上下文切换,这里的任务可以是进程也可以是线程,准确的说线程才是CPU调
度的基本单位,但是因为各个资料都这么解释上下文切换,所以上面也暂时这么介绍,只要读者心里有这个概念就好。进程间通信有几种方式?由于
各个进程不共享相同的地址空间,任何一个进程的全局变量在另一个进程中都不可见,所以如果想要在进程之间传递数据就需要通过内核,在内核中
开辟出一块区域,该区域对多个进程都可见,即可用于进程间通信。有读者可能有疑问了,文件方式也是进程间通信啊,也要在内核开辟区域吗?这
里说的内核区域其实是一段缓冲区,文件方式传输数据也有内核缓冲区的参与(零拷贝除外)。如何开辟这种公共区域来进行进程间通信呢?匿名管
道匿名管道就是pipe,pipe只能在父子进程间通信,而且数据只能单向流动(半双工通信)。使用方式:1)父进程创建管道,会得到两个
文件描述符,分别指向管道的两端;2)父进程创建子进程,从而子进程也有两个文件描述符指向同一管道;3)父进程可写数据到管道,子进程就
可从管道中读出数据,从而实现进程间通信,下面的示例代码中通过pipe实现了每秒钟父进程向子进程都发送消息的功能。#include
#include#includeintmain(){in
t_pipe[2];intret=pipe(_pipe);if(ret<0){perror(''pipe\
n'');}pid_tid=fork();if(id<0){perror(''fork
\n'');}elseif(id==0){//子进程close(_pipe[1]);
intj=0;char_mesg[100];while(j<100){
memset(_mesg,''\0'',sizeof(_mesg));read(_p
ipe[0],_mesg,sizeof(_mesg));printf(''%s\n'',_mesg);
j++;}}else{//父进程close(_pipe[0
]);inti=0;charmesg=NULL;while(i<
100){mesg=''父进程来写消息了'';write(_pipe[1],
mesg,strlen(mesg)+1);sleep(1);++i;
}}return0;}我们平时也经常使用关于管道的命令行:ls|less该命令行的流向图如下:1:创
建管道2:为ls创建一个进程,设置stdout为管理写端3:为less创建一个进程,设置stdin为管道读端高级管道通过popen
将另一个程序当作一个新的进程在当前进程中启动,它算作当前进程的子进程,高级管道只能用在有亲缘关系的进程间通信,这种亲缘关系通常指父
子进程,下面的GetCmdResult函数可以获取某个Linux命令执行的结果,实现方式就是通过popen。std::string
GetCmdResult(conststd::string&cmd,intmax_size=10240){
chardata=(char)malloc(max_size);if(data==NULL){
returnstd::string(''mallocfail'');}memset(data,0,ma
x_size);constintmax_buffer=256;charbuffer[max_buffer
];//将标准错误重定向到标准输出FILEfdp=popen((cmd+''2>&1'').c_str
(),''r'');intdata_len=0;if(fdp){while(!feof(fdp
)){if(fgets(buffer,max_buffer,fdp)){
intlen=strlen(buffer);if(data_len+len>
max_size){cout<<''datasizelargerthan''
<memcpy(data+data_len,buffer,len);data_l
en+=len;}}pclose(fdp);}std:
:stringret(data,data_len);free(data);returnret;}命名管道匿名
管道有个缺点就是通信的进程一定要有亲缘关系,而命名管道就不需要这种限制。命名管道其实就是一种特殊类型的文件,所谓的命名其实就是文件
名,文件对各个进程都可见,通过命名管道创建好特殊文件后,就可以实现进程间通信。可以通过mkfifo创建一个特殊的类型的文件,参数读
者看名字应该就了解,一个是文件名,一个是文件的读写权限:intmkfifo(constcharfilename,mode
_tmode);当返回值为0时,表示该命名管道创建成功,至于如何通信,其实就是个读写文件的问题!消息队列队列想必大家都知道,像F
IFO一样,这里可以有多个进程写入数据,也可以有多个进程从队列里读出数据,但消息队列有一点比FIFO还更高级,它读消息不一定要使用
先进先出的顺序,每个消息可以赋予类型,可以按消息的类型读取,不是指定类型的数据还存在队列中。本质上MessageQueue是存放在
内核中的消息链表,每个消息队列链表会由消息队列标识符表示,这个消息队列存于内核中,只有主动的删除该消息队列或者内核重启时,消息队列
才会被删除。在Linux中消息队列相关的函数调用如下://创建和访问一个消息队列intmsgget(key_t,key,i
ntmsgflg);//用来把消息添加到消息队列中intmsgsend(intmsgid,constvoidmsg
_ptr,size_tmsg_sz,intmsgflg);//msg_ptr是结构体数据的指针,结构第一个字段要有个类型
:structMsg{longintmessage_type;//想要传输的数据};//从消息队列中获
取消息intmsgrcv(intmsgid,voidmsg_ptr,size_tmsg_st,longintm
sgtype,intmsgflg);//用来控制消息队列,不同的command参数有不同的控制方式intmsgctl(in
tmsgid,intcommand,structmsgid_dsbuf);示例代码如下:#includeo.h>#include#include#include#incl
ude#include#include#includead>usingnamespacestd;#defineBUFFER_SIZ20typedefstruct{?long
intmsg_type;?chartext[BUFFER_SIZ];}MsgWrapper;voidReceive()
{?MsgWrapperdata;?longintmsgtype=2;?intmsgid=msgget((key_
t)1024,0666|IPC_CREAT);?if(msgid==-1){?cout<<''msggeterr
or\n'';?return;?}?while(true){?if(msgrcv(msgid,(void)&data,
BUFFER_SIZ,msgtype,0)==-1){?cout<<''error''<dl;?}?cout<<''readdata''<text)>6){//发送超过6个字符的数据,结束break;?}?}?if(msgctl(msgid,IPC_RM
ID,0)==-1){?cout<<''msgctlerror\n'';?}cout<<''Receiveok\
n'';}voidSend(){?MsgWrapperdata;?longintmsgtype=2;?intmsgi
d=msgget((key_t)1024,0666|IPC_CREAT);?if(msgid==-1){?cou
t<<''msggeterror\n'';?return;?}?data.msg_type=msgtype;?for(i
nti=0;i<10;++i){memset(data.text,0,BUFFER_SIZ);?chara
=''a''+i;?memset(data.text,a,1);?if(msgsnd(msgid,(void)&da
ta,BUFFER_SIZ,0)==-1){?cout<<''msgsnderror\n'';return;?}?s
td::this_thread::sleep_for(std::chrono::seconds(1));?}memcpy(data
.text,''1234567'',7);?if(msgsnd(msgid,(void)&data,BUFFER_SIZ
,0)==-1){?cout<<''msgsnderror\n'';?return;?}}intmain(){?s
td::threadr(Receive);?r.detach();?std::threads(Send);?s.detach(
);std::this_thread::sleep_for(std::chrono::seconds(20));?return0
;}输出:root@iZuf64idor3ej648ciairaZ:~#./a.outreaddataareaddata
breaddatacreaddatadreaddataereaddatafreaddatagreaddata
hreaddataireaddatajreaddata1234567Receiveok代码中为了演示方便使用消息队
列进行的线程间通信,该代码同样用于进程间通信,消息队列的实现依赖于内核的支持,上述代码可能在某些系统(WSL)上不能运行,在正常的
Ubuntu上可以正常运行。消息队列VS命名管道消息队列>命名管道1)消息队列收发消息自动保证了同步,不需要由进程自己来提供同步方
法,而命名管道需要自行处理同步问题;2)消息队列接收数据可以根据消息类型有选择的接收特定类型的数据,不需要像命名管道一样默认接收数
据。消息队列<命名管道消息队列有一个缺点就是发送和接收的每个数据都有最大长度的限制。共享内存可开辟中一块内存,用于各个进程间共享,
使得各个进程可以直接读写同一块内存空间,就像线程共享同一块地址空间一样,该方式基本上是最快的进程间通信方式,因为没有系统调用干预,
也没有数据的拷贝操作,但由于共享同一块地址空间,数据竞争的问题就会出现,需要自己引入同步机制解决数据竞争问题。共享内存只是一种方式
,它的实现方式有很多种,主要的有mmap系统调用、Posix共享内存以及SystemV共享内存等。通过这三种“工具”共享地址空间
后,通信的目的自然就会达到。信号信号也是进程间通信的一种方式,信号可以在任何时候发送给某一个进程,如果进程当前并未处于执行状态,内
核将信号保存,直到进程恢复到执行态再发送给进程,进程可以对信号设置预处理方式,如果对信号设置了阻塞处理,则信号的传递会被延迟直到阻
塞被取消,如果进程结束,那信号就被丢弃。我们常用的CTRL+C和kill等就是信号的一种,也达到了进程间通信的目的,进程也可以对信
号设置signal捕获函数自定义处理逻辑。这种方式有很大的缺点:只有通知的作用,通知了一下消息的类型,但不能传输要交换的任何数据。
Linux系统中常见的信号有:SIGHUP:该信号在用户终端结束时发出,通常在中断的控制进程结束时,所有进程组都将收到该信号,该信
号的默认操作是终止进程;SIGINT:程序终止信号,通常的CTRL+C产生该信号来通知终止进程;SIGQUIT:类似于程序错误信号
,通常的CTRL+\产生该信号通知进程退出时产生core文件;SIGILL:执行了非法指令,通常数据段或者堆栈溢出可能产生该信号;
SIGTRAP:供调试器使用,由断电指令或其它陷阱指令产生;SIGABRT:使程序非正常结束,调用abort函数会产生该信号;SI
GBUS:非法地址,通常是地址对齐问题导致,比如访问一个4字节长的整数,但其地址不是4的倍数;SIGSEGV:合理地址的非法访问,
访问了未分配的内存或者没有权限的内存区域;SIGPIPE:管道破裂信号,socket通信时经常会遇到,进程写入了一个无读者的管道;
SIGALRM:时钟定时信号,由alarm函数设置的时间终止时产生;SIGFPE:出现浮点错误(比如除0操作);SIGKILL:杀
死进程(不能被捕捉和忽略);信号量想必大家都听过信号量,信号量就是一个特殊的变量,程序对其访问都是原子操作,每个信号量开始都有个初
始值。最简单最常见的信号量是只能取0和1的变量,也叫二值信号量。信号量有两个操作,P和V:P:如果信号量变量值大于0,则变量值减1
,如果值为0,则阻塞进程;V:如果有进程阻塞在该信号量上,则唤醒阻塞的进程,如果没有进程阻塞,则变量值加1Q信号量和信号有什么关系
?A没有任何关系,完全是不同的东西。Q信号量与互斥量有什么区别?A互斥量用于互斥,信号量用于同步,互斥指的是某一资源同一时间只允许
一个访问者访问,但无法限制访问顺序,访问是无序的,而同步在互斥的基础上可以控制访问者对资源的顺序。套接字:就是网络传输,不用多说,
网络通信都可以多机通信呢,更不用说进程间通信啦,你能看到程序喵的文章也是套接字的功劳。文件:显而易见,多个进程可以操作同一个文件,
所以也可以通过文件来进行进程间通信。?线程同步相关术语互斥锁在多线程环境中往往存在因某一资源被同时访问导致该资源不一致的问题,互斥
锁通过排它性,即同时只允许一个访问者对其进行访问来保证资源的有效同步,但它无法限制线程对该资源的访问顺序,因此线程对资源的访问也是
无序的。自旋锁在互斥锁中,如果线程A在请求锁的时候发现该锁已被线程B霸占,那么此时线程A便会进入休眠状态,直到锁被线程
B释放后被系统唤醒。但在自旋锁中,线程A在发现锁被霸占时并不进入休眠,而是一直循环查看锁的持有者是否已经释放了该锁。初看起
来,这种霸占CPU资源的做法极其低效,但与互斥锁仔细对比后我们可以发现它有以下几个优点:实现简单:只需死循环检测锁状态即可,没有互
斥锁休眠、唤醒所涉及的一系列上下文切换、CPU抢占等各种复杂流程。高效:正是由于实现极其简单,所以它在一些情况下极其高效。当然,上
面的高效也是有条件的,由于它占用CPU资源,所以它主要适用于以下场景:临界区持锁时间较短且CPU资源不紧张。多用于多核环境。递归锁
递归锁又叫可重入锁,与互斥锁的主要区别是在同一个线程内可以多次获得锁资源,别的线程必须等待该线程释放相应次数的锁才能获得,其主要目
的是为了解决同一进程内的死锁问题,但在不同的线程中,它与互斥锁并没有什么区别。读写锁上面介绍的互斥锁、自旋锁、递归锁都属于排它锁,
即一个线程获得锁资源后,其他线程必须等待直到该锁被释放。但在某些读多写少的情况下,这样的机制难免有些低效,因此读写锁就是为解决这样
的问题而诞生的。读写锁分读锁和写锁,其特点如下:读锁:如果线程A获得了读锁,线程B可以获得读锁,但不可以获得写锁。写锁:如
果线程A获得了写锁,线程B即不可以获得读锁,也不可以获得写锁。写锁优先:如果线程A申请获得写锁,线程B申请获得读锁
,优先给线程A分配写锁。条件变量条件变量主要适用于一个线程需要等待某个共享资源满足某个条件后进行一系列同步操作的场景。它主要包含:
等待某个条件成立的等待线程。满足某个条件成立的信号发送线程。它一般与互斥锁配合使用(主要用来保护共享资源),在等待线程中,如果条件
不成立,该线程会自动阻塞,并释放掉等待状态改变的互斥锁,如果信号发送线程改变了条件,它发送信号给关联的条件变量,并唤醒等待线程,等
待线程重新获得互斥锁,重新评估条件。信号量信号量主要适用于一个线程需要等待另外一个个线程完成一些操作后再继续执行自己操作的场景,它
与上面各种锁的最大区别是:锁要解决的是共享资源的同步问题,而信号量要解决的是线程之间任务同步问题。锁必须在同一进程进行加锁和解锁操
作,而信号量可以通过一个线程中得到,在另一个线程中释放。认真回味下我们会发现信号量要解决的问题也可以使用条件变量来处理,相对于信号
量,条件变量主要有以下不足:条件变量需要借助全局共享变量以及互斥锁来达到状态的检测。条件变量适用于多线程环境,无法适用于多进程环境
。屏障屏障是一种协调多个线程进行工作,即允许某个线程等待直到所有的合作线程达到某一个条件,然后从该条件下继续执行的同步机制。相关知
识线程的上下文程序跑起来,就会产生一个线程。在这个线程里面会有一个context上下文,我们可以往context里面存放东西,随后
在线程管辖范围内都可以获取到。线程需要用的时候再创建context上下文,不用则留个占位就行了。线程上下文里面有两部分:有效载荷和
threadlocal配件。threadlocal.set()threadlocal.get()context.set(this,
1000);//配件。threadlocal是线程上下文context的代理对象,context的目的是存放数据,自然threa
dlocal也是用来存放数据,所以主要用法就是set和get操作。用户进程缓冲区和内核缓冲区用户进程和操作系统的关系这是一个计算机
系统运行时的简化模型,我们把所有运行在操作系统上的进程成为用户进程,它们都运行在用户空间。把操作系统运行的空间成为系统空间。内核态
和用户态(kernelmode和usermode),在内核态可以访问系统资源,比如:处理器cpu:cpu控制着一个程序的执行。
输入输出IO:linux有句话叫“一切都是流”,也就是所有输入输出设备的数据,包括硬盘,内存,终端都可以像流一样操作。进程管理:类
似对进程的创建,休眠,唤醒,释放之类的调度。比如linux下的fork和windows下的CreateProcess()函数。内存
:包括内存的申请,释放等管理操作。设备:这个就是常常说的外设了,比如鼠标,键盘。计时器:所有的定时任务都是以计时器为基础的。进程间
通信IPC:进程之间是不能够互相访问内存的,所以进程与进程之间的交互需要通信,而通信也是一种资源。网络通信:网络通信可以看做是进程
见通信的特殊形式。用户进程无法被直接访问系统资源,只能通过操作系统来访问,就是“系统调用”。比如下图,展示一个用户通过shell控
制计算机所经过的数据流向:文件读写和终端控制,都是通过内核进行的。intelx86CPU有四种不同的执行级别0-3,linux
只使用了其中的0级和3级分别来表示内核态和用户态。用户进程缓冲区用户进程通过系统调用访问系统资源的时候,需要切换到内核态,而这对应
一些特殊的堆栈和内存环境,必须在系统调用前建立好。而在系统调用结束后,cpu会从核心模式切回到用户模式,而堆栈又必须恢复成用户进程
的上下文。而这种切换就会有大量的耗时。程序在读取文件时,会先申请一块内存数组,称为buffer,然后每次调用read,读取设定字节
长度的数据,写入buffer。之后的程序都是从buffer中获取数据,当buffer使用完后,在进行下一次调用,填充buffer。
用户缓冲区的目的是为了减少系统调用次数,从而降低操作系统在用户态与核心态切换所耗费的时间。内核缓冲区除了在进程中设计缓冲区,内核也
有自己的缓冲区。当一个用户进程要从磁盘读取数据时,内核一般不直接读磁盘,而是将内核缓冲区中的数据复制到进程缓冲区中。但若是内核缓冲
区中没有数据,内核会把对数据块的请求,加入到请求队列,然后把进程挂起,为其它进程提供服务。等到数据已经读取到内核缓冲区时,把内核缓
冲区中的数据读取到用户进程中,才会通知进程,当然不同的io模型,在调度和使用内核缓冲区的方式上有所不同,下一小结介绍。你可以认为,
read是把数据从内核缓冲区复制到进程缓冲区。write是把进程缓冲区复制到内核缓冲区。当然,write并不一定导致内核的写动作,
比如os可能会把内核缓冲区的数据积累到一定量后,再一次写入。这也就是为什么断电有时会导致数据丢失。所以说内核缓冲区,是为了在OS级
别,提高磁盘IO效率,优化磁盘写操作。流程对比阻塞和非阻塞,在阻塞io中,直到数据从内核缓冲区拷贝到用户缓冲区才通知用户进程调用完
成并唤醒,而非阻塞,在轮训得知数据准备好后,数据还是在内核缓冲区中,等你去读取,这也就是说数据准备好,并不代表已经读好可以使用。当
然也不代表一定能读。缓冲区和缓存缓冲区的英文是buffer,而缓存的应为是cache.CPU缓存(CacheMemory)是位于
CPU与内存之间的临时存储器,因为cpu的计算速度要比内存的读写速度快很多,而把这些可能会被重复访问到的数据存储于cpu缓存中,就
会提高读取速度。可以说缓存是cpu和内存之间的临时存储器。也就是说,buffer是因为减少调用次数,集中调用,提高系统性能。而ca
che是将读取过的数据保存起来,重新读取时若命中(找到需要的数据)就不要去读内存了。如何将一个进程(线程)绑定到一个固定的CPU核
上?第一种:linux的shell命令行方式,命令名字为taskset。第二种:代码级实现:pthread_setaffinity
_np和sched_setaffinity函数接口。方法一第一种方式我已经验证过了,确实可行。此种方式有个问题,就是只有线程运行起
来后才会被绑定到某个核上,不够及时。具体的方式为:1.首先根据系统的差别运行如下安装命令:sudoapt-ge
tinstallutil-linux(Debian,UbuntuorLinuxMint)sudoyu
minstallutil-linux(Fedora,CentOSorRHEL)2.相关命令的使用
:2.1使用命令taskset-p来获得此Process的CPUaffinity。eg:tasks
et-p2915------>pid2915''scurrentaffinitymask:ff;ff=="111
11111",每一个1代表一个核,共8个核,能用的核数也为8个核。2.2使用命令taskset-cp可获
得数字形式的CPUaffinity。eg:taskset-cp2915------>pid2915''scurre
ntaffinitylist:0--7。接下来为将进程pin到某个核上的命令;2.3taskset-pSK>eg:taskset-p0x119030------>pid9030''scurrentaffi
nitymask:ff,pid9030''snewaffinitymask:11。意思就是将此进程绑定到了CPU
core0and4。2.4taskset-cpeg:taskset-cp0
,49030------>thesameasbelow.With
"-c"option,youcanspecifyalistofnumericCPUcoreIDssepar
atedbycommas,orevenincluderanges(e.g.,0,2,5,6-10).2.5tas
kseteg:taskset0x1xxxx----->"xxxx"r
epresentedthenameofoneprogram.另外:参考文章最后的位置说到,绑定到此物理核之后,别的进程(
线程)还可以调度到此核上执行,但是没说绑定的这个线程没执行完之前是否会被别的线程挤掉。根据我的观察是不会被挤掉,这我在文章的开头也
有提到。进程怎么绑定CPU进程绑定CPU的好处在多核CPU结构中,每个核心有各自的L1、L2缓存,而L3缓存是共用的。如
果一个进程在核心间来回切换,各个核心的缓存命中率就会受到影响。相反如果进程不管如何调度,都始终可以在一个核心上执行,那么其数据的L
1、L2缓存的命中率可以显著提高。所以,将进程与CPU进行绑定可以提高CPU缓存的命中率,从而提高性能。而进程与CPU
绑定被称为:CPU亲和性。设置进程的CPU亲和性在Linux系统下怎么将进程与CPU进行绑定的(也就是设置进程的
CPU亲和性)。Linux系统提供了一个名为?sched_setaffinity?的系统调用,此系统调用可以设置进程的CPU
亲和性。我们来看看?sched_setaffinity?系统调用的原型:int?sched_setaffinity(pid_t?
pid,?size_t?cpusetsize,?const?cpu_set_t?mask);下面介绍一下?sched_setaf
finity?系统调用各个参数的作用:pid:进程ID,也就是要进行绑定CPU的进程ID。cpusetsize:mask参数
所指向的CPU集合的大小。mask:与进程进行绑定的CPU集合(由于一个进程可以绑定到多个CPU上运行)。参数?mas
k?的类型为?cpu_set_t,而?cpu_set_t?是一个位图,位图的每个位表示一个CPU,如下图所示:例如,将?cpu_
set_t?的第0位设置为1,表示将进程绑定到CPU0上运行,当然我们可以将进程绑定到多个CPU上运行。我们通过一个例子来
介绍怎么通过?sched_setaffinity?系统调用来设置进程的CPU亲和性:#define?_GNU_SOURCE#i
nclude?#include?#include?#include?dlib.h>#include?#include?int?main(int?argc,?ch
ar?argv){?cpu_set_t?cpuset;?CPU_ZERO(&cpuset);????//?初始化CPU集合,将
?cpuset?置为空?CPU_SET(2,?&cpuset);??//?将本进程绑定到?CPU2?上?//?设置进程的?CPU?
亲和性?if?(sched_setaffinity(0,?sizeof(cpuset),?&cpuset)?==?-1)?{?pr
intf(''Set?CPU?affinity?failed,?error:?%s\n'',?strerror(errno));?re
turn?-1;??}?return?0;}CPU亲和性实现知道怎么设置进程的CPU亲和性后,现在我们来分析一下Linux
内核是怎样实现CPU亲和性功能的。本文使用的Linux内核版本为2.6.23Linux内核为每个CPU定义了一个
类型为?structrq?的?可运行的进程队列,也就是说,每个CPU都拥有一个独立的可运行进程队列。一般来说,CPU只会从
属于自己的可运行进程队列中选择一个进程来运行。也就是说,CPU0只会从属于CPU0的可运行队列中选择一个进程来运行,而绝不会
从CPU1的可运行队列中获取。所以,从上面的信息中可以分析出,要将进程绑定到某个CPU上运行,只需要将进程放置到其所属的?
可运行进程队列?中即可。下面我们来分析一下?sched_setaffinity?系统调用的实现,sched_setaffinity
?系统调用的调用链如下:sys_sched_setaffinity()└→sched_setaffinity()└→set_c
pus_allowed()└→migrate_task()从上面的调用链可以看出,sched_setaffinity?系统调用最
终会调用?migrate_task?函数来完成进程与CPU进行绑定的工作,我们来分析一下?migrate_task?函数的实现
:static?intmigrate_task(struct?task_struct?p,?int?dest_cpu,?stru
ct?migration_req?req){?struct?rq?rq?=?task_rq(p);?//?情况1:?//?如果
进程还没有在任何运行队列中?//?那么只需要将进程的?cpu?字段设置为?dest_cpu?即可?if?(!p->se.on_rq
?&&?!task_running(rq,?p))?{?set_task_cpu(p,?dest_cpu);?return?0;?
}?//?情况2:?//?如果进程已经在某一个?CPU?的可运行队列中?//?那么需要将进程从之前的?CPU?可运行队列中迁移到新
的?CPU?可运行队列中?//?这个迁移过程由?migration_thread?内核线程完成?//?构建进程迁移请求?init_
completion(&req->done);?req->task?=?p;?req->dest_cpu?=?dest_cpu;?
list_add(&req->list,?&rq->migration_queue);?return?1;}我们先来介绍一下?mi
grate_task?函数各个参数的意义:p:要设置CPU亲和性的进程描述符。dest_cpu:绑定的CPU编号。req:
进程迁移请求对象(下面会介绍)。所以,migrate_task?函数的作用就是将进程描述符为?p?的进程绑定到编号为?dest_c
pu?的目标CPU上。migrate_task?函数主要分两种情况来将进程绑定到某个CPU上:情况1:如果进程还没有在任何
CPU的可运行队列中(不可运行状态),那么只需要将进程描述符的?cpu?字段设置为?dest_cpu?即可。当进程变为可运行时
,会根据进程描述符的?cpu?字段来自动放置到对应的CPU可运行队列中。情况2:如果进程已经在某个CPU的可运行队列中,那
么需要将进程从之前的CPU可运行队列中迁移到新的CPU可运行队列中。迁移过程由?migration_thread?内核线程
完成,migrate_task?函数只是构建一个进程迁移请求,并通知?migration_thread?内核线程有新的迁移请求需要
处理。而进程迁移过程由?__migrate_task?函数完成,我们来看看?__migrate_task?函数的实现:static
?int?__migrate_task(struct?task_struct?p,?int?src_cpu,?int?dest_
cpu){?struct?rq?rq_dest,?rq_src;?int?ret?=?0,?on_rq;?...?rq_src
?=?cpu_rq(src_cpu);????//?进程所在的原可运行队列?rq_dest?=?cpu_rq(dest_cpu);
??//?进程希望放置的目标可运行队列?...?on_rq?=?p->se.on_rq;??//?进程是否在可运行队列中(可运行状
态)?if?(on_rq)?deactivate_task(rq_src,?p,?0);??//?把进程从原来的可运行队列中删除?
set_task_cpu(p,?dest_cpu);?if?(on_rq)?{?activate_task(rq_dest,?p,
?0);???//?把进程放置到目标可运行队列中?...?}?...?return?ret;}__migrate_task?函数主
要完成以下两个工作:把进程从原来的可运行队列中删除。把进程放置到目标可运行队列中。其工作过程如下图所示(将进程从CPU0的可运
行队列迁移到CPU3的可运行队列中):如上图所示,进程原本在CPU0的可运行队列中,但由于重新将进程绑定到CPU3,所以
需要将进程从CPU0的可运行队列迁移到CPU3的可运行中。迁移过程首先将进程从CPU0的可运行队列中删除,然后再将进程
插入到CPU3的可运行队列中。当CPU要运行进程时,首先从它所属的可运行队列中挑选一个进程,并将此进程调度到CPU中运
行。总结从上面的分析可知,其实将进程绑定到某个CPU只是将进程放置到CPU的可运行队列中。由于每个CPU都有一个可运行
队列,所以就有可能会出现CPU间可运行队列负载不均衡问题。如CPU0可运行队列中的进程比CPU1可运行队列多非常多,从
而导致CPU0的负载非常高,而CPU1负载非常低的情况。当出现上述情况时,就需要对CPU间的可运行队列进行重平衡操作,
有兴趣的可以自行阅读源码或参考相关资料。线程数,设多少合理?工作线程数不是设置的越大越好服务器CPU核数有限,能够同时并发的线程数
有限,线程切换有开销,如果线程切换过于频繁,反而会使性能降低调用sleep()函数的时候,线程是否一直占用CPU?不占用,休眠时会
把CPU让出来,给其他需要CPU资源的线程使用。不止sleep,一些阻塞调用,都会让出CPU资源。例如网络编程中的:阻塞accep
t(),等待客户端连接阻塞recv(),等待下游回包单核CPU,设置多线程能否提高并发性能?即使是单核,使用多线程也是有意义的,大
多数情况也能提高并发:多线程编码可以让代码更加清晰,例如:IO线程收发包,Worker线程进行任务处理,Timeout线程进行超时
检测如果有一个任务一直占用CPU资源在进行计算,此时增加线程并不能增加并发,例如以下代码会一直占用CPU,并使得CPU占用率达到1
00%:?while(1){i++;}通常来说,Worker线程一般不会一直占用CPU进行计算,此时即使CPU是单核,增加Wo
rker线程也能够提高并发,因为这个线程在休息的时候,其他的线程可以继续工作常见服务线程模型有几种?互联网常见的服务线程模型有两种
:IO线程与工作线程通过任务队列解耦纯异步第一种,IO线程与工作线程通过队列解耦类模型。如上图,大部分Web-Server与服务框
架都是使用这样的一种“IO线程与Worker线程通过队列解耦”类线程模型:有少数几个IO线程监听上游发过来的请求,并进行收发包(生
产者)有一个或者多个任务队列,作为IO线程与Worker线程异步解耦的数据传输通道(临界资源)有多个工作线程执行正真的任务(消费者
)这个线程模型应用很广,符合大部分场景,这个线程模型的特点是,工作线程内部是同步阻塞执行任务的,因此可以通过增加Worker线程数
来增加并发能力,今天要讨论的重点是“该模型Worker线程数设置为多少能达到最大的并发”。第二种,纯异步线程模型。没有阻塞,这种线
程模型只需要设置很少的线程数就能够做到很高的吞吐量,该模型的缺点是:如果使用单线程模式,难以利用多CPU多核的优势程序员更习惯写同
步代码,callback的方式对代码的可读性有冲击,对程序员的要求也更高框架更复杂,往往需要server端收发组件,server端
队列,client端收发组件,client端队列,上下文管理组件,有限状态机组件,超时管理组件的支持however,这个模型不是今
天讨论的重点。第一类“IO线程与工作线程通过队列解耦”类线程模型,工作线程的工作模式是怎么样的?了解工作线程的工作模式,对量化分析
线程数的设置非常有帮助:上图是一个典型的工作线程的处理过程,从开始处理start到结束处理end,该任务的处理共有7个步骤:(1)
从工作队列里拿出任务,进行一些本地初始化计算,例如http协议分析、参数解析、参数校验等;(2)访问cache拿一些数据;(3)拿
到cache里的数据后,再进行一些本地计算,这些计算和业务逻辑相关;(4)通过RPC调用下游service再拿一些数据,或者让下游
service去处理一些相关的任务;(5)RPC调用结束后,再进行一些本地计算,怎么计算和业务逻辑相关;(6)访问DB进行一些数据
操作;(7)操作完数据库之后做一些收尾工作,同样这些收尾工作也是本地计算,和业务逻辑相关;分析整个处理的时间轴,会发现:其中1,3
,5,7步骤中(上图中粉色时间轴),线程进行本地业务逻辑计算时需要占用CPU而2,4,6步骤中(上图中橙色时间轴),访问cache
、service、DB过程中线程处于一个等待结果的状态,不需要占用CPU,进一步的分解,这个“等待结果”的时间共分为三部分:2.1
)请求在网络上传输到下游的cache、service、DB2.2)下游cache、service、DB进行任务处理2.3)cach
e、service、DB将报文在网络上传回工作线程如何量化分析,并合理设置工作线程数呢?通过上面的分析,Worker线程在执行的过
程中:有一部计算时间需要占用CPU另一部分等待时间不需要占用CPU通过量化分析,例如打日志进行统计,可以统计出整个Worker线程
执行过程中这两部分时间的比例,例如:执行计算,占用CPU的时间(粉色时间轴)是100ms等待时间,不占用CPU的时间(橙色时间轴)
也是100ms得到的结果是,这个线程计算和等待的时间是1:1,即有50%的时间在计算(占用CPU),50%的时间在等待(不占用CP
U):假设此时是单核,则设置为2个工作线程就可以把CPU充分利用起来,让CPU跑到100%假设此时是N核,则设置为2N个工作现场就
可以把CPU充分利用起来,让CPU跑到N100%结论N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作
线程数(线程池线程数)设置为N(x+y)/x,能让CPU的利用率最大化。一般来说,非CPU密集型的业务(加解密、压缩解压缩、搜
索排序等业务是CPU密集型的业务),瓶颈都在后端数据库访问或者RPC调用,本地CPU计算的时间很少,所以设置几十或者几百个工作线程
是能够提升吞吐量的。进程的基本状态及转换和阻塞及挂起的理解就绪状态:一个进程获得了除处理机外的一切所需资源,一旦得到处理机即可运行
,则称此进程处于就绪状态。执行状态:当一个进程在处理机上运行时,则称该进程处于运行状态。阻塞状态:一个进程正在等待某一事件发生(例
如请求I/O而等待I/O完成等)而暂时仃止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。挂起状态:由于IO的
速度慢于CPU的运算速度,经常出现CPU等待I/O的情况。这时OS需要将主存中的进程对换至外存。在进程行为模式中需要增加一个新的挂
起(suspend)状态。当内存中所有进程阻塞时,OS可将一进程置为挂起态并交换到外存,再调入另一个进程执行。新建状态:进程刚创建
,但还不能运行,OS还没有把它加到可执行进程组中,通常是还没有加载到主存中的新进程。退出状态:OS从可执行进程组中释放出的进程,或
者是因为它自身停止了,或者是因为某种原因被取消。进程不在适合执行,但与作业相关的表和其它信息临时被OS保留起来,为其他程序提供所需
信息。活跃就绪:指进程在主存并旦可被调度的状态。静止就绪:指进程被对换到辅存时的就绪状态,是不能被直接调度的状态,只有当主存中没有
活跃就绪态进程,或者是挂起态进程具有更高的优先级,系统将把挂起就绪态进程调回主存并转换为活跃就绪。活跃阻塞:指进程在主存中。一旦等
待的事件产生,便进入活跃就绪状态。静止阻塞:指进程对换到辅存时的阻塞状态。一旦等待的事件产生,便进入静止就绪状态。进程转换状态图三
种基本状态转换图:五种基本状态转换图(单挂起):五种基本状态转换图(双挂起):阻塞及挂起的理解挂起是一种主动行为,是把一个进程从内
存转到外存,而阻塞则是一种被动行为(并不绝对,看个人理解),是在等待事件或资源时任务的表现。对于挂起,其进程所有资源都转入外存;而
阻塞,其进程所有资源依然保存在内存中。对应挂起的行为是激活,即当没有活动就绪进程时或静止就绪队列里面有进程优先级高于活动就绪里面所
有进程,还有就是当一个进程释放足够内存时,系统会把一个高优先级阻塞激活。而阻塞却没有对应的主动行为来解除,需要其他进程或系统唤醒。
一个比喻:只有一个锅,当食材已经准备充分的菜就可以下锅,这就是执行态;而其他准备好的菜就顺序放在一旁,等待下锅,这就是就绪态;还有
的菜准备还不是很充分,例如该切片的菜还没有切片,还有一些佐料还没有从冰箱里拿出,这就是I/O操作等待,类似阻塞态;而还有一种情况就
是厨房的面积太小了,桌子上放不下那么多的准备下锅的菜,于是先将那些还没有准备好的菜放入冰箱,如果还是放不了那么多菜,就把那些已经准
备好的但没有下锅的菜放入冰箱,以免变质,这就是挂起态;当桌子上有位置了,就把菜从冰箱拿出来,这便是激活操作。做好的菜就可以端出厨房
,这便是退出态。(PS:这里我假设冰箱空间很大,也存在那种炒到一半的菜放到一边的情况,即从执行态到就绪态或阻塞态的情况,这种情况可
以类似川菜中先将肉处理一下(去腥等等),然后又从锅里捞出来,用豆瓣和其他佐料来进行腌制(这里如果佐料没有在桌子上就类似进入阻塞态,
而如果有就相当于进入就绪态,但是锅里已经被占用了,需要等待一会儿)至于优先级的高低,也可类比有些菜是为了其他菜做铺垫(举个例子:在
川菜里面,回锅肉和炒白菜,一般家庭会优先做回锅肉,因为当做回锅肉时,会炒出许多猪油,而这些有可以用来炒白菜,这样的炒白菜不仅好吃,
而且也节约了许多油。))多线程编程线程状态转换线程的状态转换是线程控制的基础。线程状态总的可分为五大状态:分别是新建、就绪、运行、
等待/阻塞、死亡。用一个图来描述如下:新建状态:线程对象已经创建,还没有在其上调用start()方法。就绪状态:当线程有资格运行,
但调度程序还没有把它选定为运行线程时线程所处的状态。当start()方法调用时,线程首先进入可运行状态。在线程运行之后或者从阻塞、
等待或睡眠状态回来后,也返回到就绪状态。运行状态:线程调度程序从就绪线程池中选择一个线程作为当前线程时线程所处的状态。这也是线程进
入运行状态的唯一一种方式。等待/阻塞/睡眠状态:线程不会被分配CPU时间,无法执行;可能阻塞于I/O,或者阻塞于同步锁。实际上
这个三状态组合为一种,其共同点是:线程仍旧是活的,但是当前没有条件运行。换句话说,它是可运行的,但是如果某件事件出现,他可能返回到
可运行状态。死亡态:当线程的run()方法完成时就认为它死去,调用stop()或destroy()亦有同样效果,但是不被推荐
,前者会产生异常,后者是强制终止,不会释放锁。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生
。如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException
异常。锁机制线程锁机制的本质是解决线程通信中的互斥问题。由于我们可以通过private关键字来保证数据对象只能被方法访问,所
以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法和
synchronized块。注意:每个类实例对应一把锁,同步和互斥等都是相对多线程而言的。synchronized方法通过在方
法声明中加入synchronized关键字来声明synchronized方法,语法如下:publicsynchronize
dvoidprocData();synchronized方法原理:多个线程访问同一个synchronized方法时,必须
获得调用该方法的类实例的锁才能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能
获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为synchronized的成员函数中至多只有
一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方
法均被声明为synchronized)。在Java中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明
为synchronized,以控制其对类的静态成员变量的访问。synchronized方法的缺陷:若将一个大的方法声明为sy
nchronized将会大大影响效率,典型地,若将线程类的方法run()声明为synchronized,由于在线程的整个
生命期内它一直在运行,因此将导致它对本类任何synchronized方法的调用都永远不会成功。当然我们可以通过将访问类成员变量
的代码放到专门的方法中,将其声明为synchronized,并在主方法中调用来解决这一问题,但是Java为我们提供了更好的
解决办法,那就是synchronized块。synchronized块通过synchronized关键字来声明synchr
onized块,语法如下:synchronized(syncObject){//允许访问控制的代码}synchronized
块是这样一个代码块,其中的代码必须获得对象syncObject(如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由
于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。阻塞机制阻塞机制的本质是为了解决线程通信的同步问题。锁和阻塞机制解决线
程通信中的互斥和同步问题。为了解决对共享存储区的访问冲突,引入了锁机制,考察多个线程对共享资源的访问,显然锁机制已经不够了,因为在
任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,引入
了对阻塞机制的支持。阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。Java提供了大量方法来支持阻塞,下面让我们
逐一分析。sleep()方法sleep()允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到C
PU时间,指定的时间一过,线程重新进入可执行状态。典型地,sleep()被用在等待某个资源就绪的情形:测试发现条件不满足后,让
线程阻塞一段时间后重新测试,直到条件满足为止。suspend()和resume()方法两个方法配套使用,suspend()使得线程
进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。典型地,suspend()
和resume()被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用re
sume()使其恢复。yield()方法yield()使得线程放弃当前分得的CPU时间,但是不使线程阻塞,即线程仍处于可执
行状态,随时可能再次分得CPU时间。调用yield()的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。
wait()和notify()方法两个方法配套使用,wait()使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单
位的一段时间作为参数,另一种没有参数,前者当对应的notify()被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对
应的notify()被调用。阻塞方法比较2和4区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),
而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。首先,前面叙述的所有方法都隶属于Thread类,但是这一对却直接隶
属于Object类,也就是说,所有对象都拥有这一对方法。因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对
象的wait()方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的notify()方法则导致因调用该对象的wait(
)方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。其次,前面叙述的所有方法都可在任何位置调用,但是这一
对方法却必须在synchronized方法或块中调用,理由也很简单,只有在synchronized方法或块中当前线程才占有锁
,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这
样的synchronized方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException异常。wait()和notify()方法的上述特性决定了它们经常和synchronized方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的结合用于解决各种复杂的线程间通信问题。关于wait()和notify()方法调用notify()方法导致解除阻塞的线程是从因调用该对象的wait()方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。除了notify(),还有一个方法notifyAll()也可起到类似作用,唯一的区别在于,调用notifyAll()方法将把因调用该对象的wait()方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend()方法和不指定超时期限的wait()方法的调用都可能产生死锁。遗憾的是,Java并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。以上我们对Java中实现线程阻塞的各种方法作了一番分析,我们重点分析了wait()和notify()方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。关于join()方法join()方法可用于让当前线程阻塞,以等待特定线程(调用join的线程)的消亡。不允许线程对象在自己的可执行体中调用自己线程的join。线程优先级线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得CPU时间时,线程调度系统根据各个线程的优先级来决定给谁分配CPU时间,优先级高的线程有更大的机会获得CPU时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。可以调用Thread类的方法getPriority()和setPriority()来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,缺省是5(NORM_PRIORITY)。守护线程与用户线程线程可以分为用户线程(User)和守护线程(Daemon):守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。可以通过调用方法isDaemon()来判断一个线程是否是守护线程,也可以调用方法setDaemon()来将一个线程设为守护线程。个人对Daemon线程更直观的理解是:无论User还是Daemon线程,都具有可执行序列,拥有自己的工作栈,区别在于:Daemon线程会随着其父线程结束而结束,它不属于程序本体。另一层意思,父线程的结束取决于其所有子User线程,而与daemon线程无关。它们之间的不同决定了它们用于不同的场景,守护线程一般为其他线程提供服务,如垃圾回收器。需要注意的是setDaemon()方法必须在线程对象没有调用start()方法之前调用,否则没效果。线程组机制线程组是一个Java特有的概念,在Java中,线程组是类ThreadGroup的对象,每个线程都隶属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。你可以通过调用包含ThreadGroup类型参数的Thread类构造函数来指定线程属的线程组,若没有指定,则线程缺省地隶属于名为system的系统线程组。除了预建的系统线程组外,所有线程组都必须显式创建。除系统线程组外的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶属的线程组,若没有指定,则缺省地隶属于系统线程组。这样,所有线程组组成了一棵以系统线程组为根的树。Java允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。Java的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。Java的ThreadGroup类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。ThreadLocaljava.lang.ThreadLocal是localvariable(线程局部变量)。它为每一个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。ThreadLocal本质是一个线程安全的hashMap,key为threadName,Value为线程内的变量。Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据对象提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现锁机制,比ThreadLocal更加复杂。小结多线程的核心在于多个代码块并发执行,本质特点在于各代码块之间的代码是乱序执行的。
献花(0)
+1
(本文系汉无为首藏)