进程,线程,协程,并行与并发区别, 上下文

2 篇文章 0 订阅
2 篇文章 0 订阅

进程 
进程的出现是为了更好的利用CPU资源使到并发成为可能。 假设有两个任务A和B,当A遇到IO操作,CPU默默的等待任务A读取完操作再去执行任务B,这样无疑是对CPU资源的极大的浪费。聪明的老大们就在想若在任务A读取数据时,让任务B执行,当任务A读取完数据后,再切换到任务A执行。注意关键字切换,自然是切换,那么这就涉及到了状态的保存,状态的恢复,加上任务A与任务B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录任务A和任务B分别需要什么资源,怎样去识别任务A和任务B等等。登登登,进程就被发明出来了。通过进程来分配系统资源,标识任务。如何分配CPU去执行进程称之为调度,进程状态的记录,恢复,切换称之为上下文切换。进程是系统资源分配的最小单位,进程占用的资源有:地址空间,全局变量,文件描述符,各种硬件等等资源。 
线程 
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西——-文本内容,不停的切换造成性能上的损失。若有一种机制,可以使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。是的,这种机制就是线程。线程共享进程的大部分资源,并参与CPU的调度, 当然线程自己也是拥有自己的资源的,例如,栈,寄存器等等。 此时,进程同时也是线程的容器。线程也是有着自己的缺陷的,例如健壮性差,若一个线程挂掉了,整一个进程也挂掉了,这意味着其它线程也挂掉了,进程却没有这个问题,一个进程挂掉,另外的进程还是活着。 
协程 
协程通过在线程中实现调度,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈。 当涉及到大规模的并发连接时,例如10K连接。以线程作为处理单元,系统调度的开销还是过大。当连接数很多 —> 需要大量的线程来干活 —> 可能大部分的线程处于ready状态 —> 系统会不断地进行上下文切换。既然性能瓶颈在上下文切换,那解决思路也就有了,在线程中自己实现调度,不陷入内核级别的上下文切换。说明一下,在历史上协程比线程要出现得早,在1963年首次提出, 但没有流行开来。为什么没有流行,没有找到信服的资料,先挖个坑,以后那天了解后,再补上。 
小结 
进程,线程,协程不断突破,更高效的处理阻塞,不断地提高CPU的利用率。但是并不是说,线程就一定比进程快,而协程就一定不线程要快。具体还是要看应用场景。可以简单粗暴的把应用分为IO密集型应用以及CPU密集型应用。 
多核CPU,CPU密集型应用 
此时多线程的效率是最高的,多线程可以使到全部CPU核心满载,又避免了协程间切换造成性能损失。当CPU密集型任务时,CPU一直在利用着,切换反而会造成性能损失,即便协程上下文切换消耗最小,但也还是有消耗的。 
多核CPU,IO密集型应用 
此时采用多线程多协程效率最高,多线程可以使到全部CPU核心满载,而一个线程多协程,则更好的提高了CPU的利用率。 
单核CPU,CPU密集型应用 
单进程效率是最高,此时单个进程已经使到CPU满载了。 
单核CPU,IO密集型应用 
多协程,效率最高。例如,看了上面应该也是知道的了 
并发与并行 
并行 
并行就是指同一时刻有两个或两个以上的“工作单位”在同时执行,从硬件的角度上来看就是同一时刻有两条或两条以上的指令处于执行阶段。所以,多核是并行的前提,单线程永远无法达到并行状态。可以利用多线程和度进程到达并行状态。另外的,Python的多线程由于GIL的存在,对于Python来说无法通过多线程到达并行状态。 
并发 
对于并发的理解,要从两方面去理解,1.并发设计 2.并发执行。先说并发设计,当说一个程序是并发的,更多的是指这个程序采取了并发设计。 
并发设计的标准:使多个操作可以在重叠的时间段内进行 ,这里的重点在于重叠的时间内, 重叠时间可以理解为一段时间内。例如:在时间1s秒内, 具有IO操作的task1和task2都完成,这就可以说是并发执行。所以呢,单线程也是可以做到并发运行的。当然啦,并行肯定是并发的。一个程序能否并发执行,取决于设计,也取决于部署方式。例如, 当给程序开一个线程(协程是不开的),它不可能是并发的,因为在重叠时间内根本就没有两个task在运行。当一个程序被设计成完成一个任务再去完成下一个任务的时候,即便部署是多线程多协程的也是无法达到并发运行的。 
并行与并发的关系: 并发的设计使到并发执行成为可能,而并行是并发执行的其中一种模式。

//unity 
协程(Coroutine)并不是真正的多线程

说到Coroutine,我们必须提到两个更远的东西。在操作系统(os)级别,有进程(process)和线程(thread)两个(仅从我们常见的讲)实际的“东西”(不说概念是因为这两个家伙的确不仅仅是概念,而是实际存在的,os的代码管理的资源)。这两个东西都是用来模拟“并行”的,写操作系统的程序员通过用一定的策略给不同的进程和线程分配CPU计算资源,来让用户“以为”几个不同的事情在“同时”进行“。在单CPU上,是os代码强制把一个进程或者线程挂起,换成另外一个来计算,所以,实际上是串行的,只是“概念上的并行”。在现在的多核的cpu上,线程可能是“真正并行的”。

Coroutine,翻译成”协程“,初始碰到的人马上就会跟上面两个概念联系起来。直接先说区别,Coroutine是编译器级的,Process和Thread是操作系统级的。Coroutine的实现,通常是对某个语言做相应的提议,然后通过后成编译器标准,然后编译器厂商来实现该机制。Process和Thread看起来也在语言层次,但是内生原理却是操作系统先有这个东西,然后通过一定的API暴露给用户使用,两者在这里有不同。Process和Thread是os通过调度算法,保存当前的上下文,然后从上次暂停的地方再次开始计算,重新开始的地方不可预期,每次CPU计算的指令数量和代码跑过的CPU时间是相关的,跑到os分配的cpu时间到达后就会被os强制挂起。Coroutine是编译器的魔术,通过插入相关的代码使得代码段能够实现分段式的执行,重新开始的地方是yield关键字指定的,一次一定会跑到一个yield对应的地方。

一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制

协程、线程和执行上下文 
转载

摘要: 本文介绍协程、线程及它们执行的上下文等概念,同时给出注意事项。协程是用户级的任务调度,线程是内核级的任务调度,而任务调度过程都涉及到上下文切换(保存与恢复),本文将从较为深刻的角度来阐述这些概念,及其相互关系。 
协程和线程 
线程在现代的系统里扮演的角色很重要,几乎一个现代一点的,稍微复杂大型一点的程序,往往都会引入线程,实现某种意义的并行。线程存在广泛的支持,从操作系统,到编译器,再到语言的定义与实现者,他们都在努力让线程更好用,也有更少的限制。相反协程的现实要惨淡很多,在操作系统那里很少有协程的影子;只有Windows里,存在一个协程的实现——fiber(在那里它被称为纤程)。 
从概念上来说,线程和协程最大的区别就是调度方式不同——线程是被动调度的,协程是主动调度的。也就是说,线程什么时候执行,什么时候不执行,完全是OS决定,线程是OS的菜,程序员最多只能主动让线程停下来,却很难让线程主动运行起来。相反协程的执行与停止完全由程序员决定,何时执行、何时停止简单地调用一个协助方法就可以完成。因为线程是被动调度的,所以线程需要大量同步方法;而协程是主动调度的,所以协程之间的同步可以很自然完成——该停该跑——就这简单直接。 
协程的概念很完美,使用可能也很方便。但是因为一些我还不了解的原因,在C/C++里并没有广泛使用协程库可用,这是因为现在的OS和编译器无法很好地支持协程的实现;这也许是历史原因,线程在概念上与线程不相矛盾,但它的许多实现方式限制了实现通用安全的协程。在C语言和UNIX草莽朝代,实现一个协程要比实现一个函数(协程的一种特例)更困难,而对于当时来说,函数已经完全够用了,如此协程被冷淡了。 
在用户层协程被死死地限制了,可是在Linux内核里,它却一个基本的构件——内核线程,在更多的时候,更一个协程。当然这句话是我自己说,在我很有限的Linux内核经验上说的,极有可能是错误的。 
执行上下文 
线程是操作系统实现的,也是操作系统的调度单位。我们知道,所谓调度就是操作系统根据一些规则决定在何时中止一个线程操作,并在一个恰当的时候再唤醒这个线程。这里有个自然的问题,就是操作系统怎么实现中止与唤醒操作的?其时停止与唤醒操作都离不开CPU的支持,CPU在硬件上实现了中断机制,OS使用中断机制实现了线程调度。OS可以告诉CPU,每过100ms(毫秒)或10ms你就调用一下一段代码——这就是所谓的时钟中断例程,OS在这一小段代码里实现了线程的中止与唤醒。在这段代码里,OS通常会做这些事: 
保存一下当前的状态,当前是哪个线程在执行,当前这个线程执行到哪里了,等等——这些状态实际上就是当前CPU或CPU-Core里的数个特殊寄存器的数值。我们称这些状态可能放在一个结构体里,这个结构体又被称为上下文(执行上下文)。 
干一些其它的事,如更新系统时间,看看有没有什么其它OS需要定时去做的事,等等。 
根据一些规则(也就是调度策略),在所有已经中止的线程挑选一个,把之前保存起来的状态恢复——实际上就是把之前保存在结构体的寄存器的值,再加载到相应寄存器里。 
最后,OS执行一个跳转指令,把CPU的执行权限传递给刚才挑选的线程,如此这个线程就从之前停下来的地方接着跑起来了。也就是说,它被唤醒了。 
在上面的说明,我们看到一次保存状态和一次恢复状态,所谓状态就是上下文(状态),而这个一次保存和一次恢复的过程称为一次上下文切换——大概意思就是说,刚才CPU在干这块的活,现在它又切换到另一块地方干活了。 
现在我们知道上下文切换,以及OS是如何实现上下文切换的了,那么有一个问题,就是程序员能不能在自己的程序里使用与OS相同功能的代码,在自己的程序进行上下文切换?答案是: 完全可以。事实上,这就是实现协程的基本方法。程序在执行的时候是没有函数这些概念的,诸如函数这些概念都上层语言及编译器实现的抽象概念,CPU不认识。CPU只认识指令及指令操作的地址。 
了解 C++ Boost 库的人一定知道它最近新加了两个库: context 和 coroutine —— 看这个名字就知道,前者是实现了上下文切换的,后者是利用前者实现了用户态协程的。

go语言的特色之一就是goroutine。也就是go协程。由于协程这个东西在go语言之前,用到相对比较少,大家对协程的理解程度不一,或有偏差。比如本人刚接触goroutine时,就对其比较畏惧,因为不知道它到底是如何运作的。因此有必要深入了解下什么是协程,它的今生前世,以及工作原理 
前世 
作为服务器端程序员,一般来说,都会使用过、或者自己实现过 “通用的异步任务系统” ,来达成安全方便的多线程使用。通常来讲,比较典型的会是基于actor模型及回调的方式制定差异。 
这里我们主要来考察下其不足之处。下面简单的画一下 任务对象和线程间的关系:

——- ——- ——- 
| task1 | | task2 | …. | taskn | => thread1 
——- ——- ——-

——- ——- ——- 
| taska | | taskb | …. | taskz | => thread2 
——- ——- ——- 
… 
——- ——- ——- 
| taskA | | taskB | …. | taskZ | => threadn 
——- ——- ——-

如图所示,一般会开n个线程来处理,每个任务按一定的策略被投递到线程中执行,任务完成后,触发异步回调。 
假设 任务中,有一定比例的任务是IO阻碍的。那么线程在执行这个任务时,会被挂起。导致后面的任务也只能等待。 
总结下其不足的地方: 
* CPU能力不能达到完全释放 
* IO阻塞任务的数量与线程被系统调度成正比 
* 任务完成需要回调方式,编程上不直观、比较难受(若没有前两条不足,这个大概也不会出现。背锅侠是也…)

(ps. 协程起源于单CPU单线程时代。要解决的问题,同这里表述的。多线程后,为了榨干CPU处理能力,协程开始用于多线程系统,如这里描述的) 
程序员的智慧 
那么如何才能把 “异步任务系统” 做的完美呢。假设能这样就好了: 
* IO阻塞任务,执行到阻塞语句时,系统可以下达它的IO指令又可以把它拎出来,重新插到任务队列最后。

假设能实现这样的效果。那么上述的不足也就不存在了: 
* 阻塞的任务因为拎出来了,后续的任务可以继续欢快的在该线程上跑了 
* 阻塞的任务因为拎出来了,线程也不会被阻塞,也就不会被系统调度出去了

那么如何才能做到。聪明的程序员很快找到了解决方案: 
* 将IO阻塞的API hook掉,换成异步实现 
* 模仿操作系统线程的调度方法,实现任务的切出切进 
这里点下,为什么需要 “实现任务的切出切进”。由于把IO阻塞的API hook掉,换成异步实现。如果让该任务继续执行的话,就会改变该任务的流程。因此必须切出去。等再次切进来时,检查IO事件是否已经到了。到了则如同 IO阻塞完毕,继续执行任务流程。否则再次切出。

我们可以看到go关键字很方便的就实现了并发编程。 上面的多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。 
runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine。 
默认情况下,在Go 1.5将标识并发系统线程个数的runtime.GOMAXPROCS的初始值由1改为了运行环境的CPU核数。 
但在Go 1.5以前调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。

执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
 

参考文献:

https://blog.csdn.net/qiuyoujie/article/details/79361470

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值