进程,线程和协程 并行与并发

谁说我不能喝 提交于 2020-01-28 02:56:57

一、进程

进程的出现是为了更好的利用CPU资源使到并发成为可能。 假设有两个任务A和B,当A遇到IO操作,CPU默默的等待任务A读取完操作再去执行任务B,这样无疑是对CPU资源的极大的浪费。聪明的老大们就在想若在任务A读取数据时,让任务B执行,当任务A读取完数据后,再切换到任务A执行。注意关键字切换,自然是切换,那么这就涉及到了状态的保存,状态的恢复,加上任务A与任务B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录任务A和任务B分别需要什么资源,怎样去识别任务A和任务B等等。登登登,进程就被发明出来了。通过进程来分配系统资源,标识任务。如何分配CPU去执行进程称之为调度,进程状态的记录,恢复,切换称之为上下文切换。进程是系统资源分配的最小单位,进程占用的资源有:

  • 地址空间
  • 全局变量
  • 文件描述符
  • 各种硬件资源

相比线程和协程,进程是比较重量级的,它需要的资源很多。进程之间不共享内存变量,所以进程间的通信方式也多种多样:

  • TCP
  • REDIS等数据库
  • 管道,文件等

二、线程

线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使得进程内并发成为可能。假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西-------文本内容,不停的切换造成性能上的损失。若有一种机制,可以使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。是的,这种机制就是线程。线程共享进程的大部分资源,并参与CPU的调度, 当然线程自己也是拥有自己的资源的,例如,栈,寄存器等等。 此时,进程同时也是线程的容器。线程也是有着自己的缺陷的,例如健壮性差,若一个线程挂掉了,整一个进程也挂掉了,这意味着其它线程也挂掉了,进程却没有这个问题,一个进程挂掉,另外的进程还是活着。

三、协程

协程是一种用户态的轻量级线程,又称微线程、纤程,英文名Coroutine。
协程通过在线程中实现调度,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈。 当涉及到大规模的并发连接时,例如10K连接。以线程作为处理单元,系统调度的开销还是过大。当连接数很多 —> 需要大量的线程来干活 —> 可能大部分的线程处于ready状态 —> 系统会不断地进行上下文切换。既然性能瓶颈在上下文切换,那解决思路也就有了,在线程中自己实现调度,不陷入内核级别的上下文切换。

在历史上协程比线程要出现得早,在1963年首次提出, 但没有流行开来。协程很古老,它曾经存在于 windows 3.2 和 早期的 JVM,但是为什么现在的 windows 和 jvm 不采用协程的模型。历史上是先有协程,是OS用来模拟多任务并发,但是因为它是非抢占式的,导致多任务时间片不能公平分享,所以后来全部废弃了协程改成抢占式的线程。

相比线程,协程的优势在于:

  • 最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
  • 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
    因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
import time

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        time.sleep(1)
        r = '200 OK'

def produce(c):
    c.next()
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

if __name__=='__main__':
    c = consumer()
    produce(c)

Python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。

小结

进程,线程,协程是执行任务的三种单位,这三种单位并没有优劣之分,而是具有不同粒度的执行单位,各自有各自的使用场景,没有一统天下的方法。特殊问题特殊处理,找准问题本身的特点是高效解决问题的必由之路。

线程不一定比进程快,协程不一定比线程快。具体还是要看应用场景,可以简单粗暴的把应用分为IO密集型应用以及CPU密集型应用。

进程和线程的区别:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程

(2)资源分配给进程,进程是程序的主体,同一进程的所有线程共享该进程的所有资源

(3)cpu分配给线程,即真正在cpu上运行的是线程

(4)线程是最小的执行单元,进程是最小的资源管理单元

协程的特点在于是一个线程执行,与多线程相比,其优势体现在:

协程的执行效率非常高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
协程不需要多线程的锁机制。在协程中控制共享资源不加锁,只需要判断状态就好了。

多核CPU,CPU密集型应用

此时多线程的效率是最高的,多线程可以使到全部CPU核心满载,又避免了协程间切换造成性能损失。当CPU密集型任务时,CPU一直在利用着,切换反而会造成性能损失,即便协程上下文切换消耗最小,但也还是有消耗的。

多核CPU,IO密集型应用

此时采用多线程多协程效率最高,多线程可以使到全部CPU核心满载,而一个线程多协程,则更好的提高了CPU的利用率。

单核CPU,CPU密集型应用

单进程效率是最高,此时单个进程已经使到CPU满载了。

单核CPU,IO密集型应用

多协程,效率最高。
线程比进程效率高。


四、并行和并发

并行
并行就是指同一时刻有两个或两个以上的“工作单位”在同时执行,从硬件的角度上来看就是同一时刻有两条或两条以上的指令处于执行阶段。所以,多核是并行的前提,单线程永远无法达到并行状态。可以利用多线程和度进程到达并行状态。另外的,Python的多线程由于GIL的存在,对于Python来说无法通过多线程到达并行状态,但是它却是并发的。

并发
并发是指在某一个时间段内,多个任务都得到了执行。有的人认为并发还有一个约束,每一时刻至多只有一个任务得到执行。

并行与并发的关系: 并发的设计使到并发执行成为可能,而并行是并发执行的其中一种模式。

并行跟并发的区别一言以蔽之:就是时间段和时刻的区别。
并行是某一时刻有多个任务在执行。
并发是某一时间段有多个任务在执行。

如果说并发的概念允许同一时刻执行多个任务,那么可以说并行是特殊的并发。

JS中的事件模型其实是单线程的,每时每刻只有一个函数在执行,但是某个时间段内可能多个任务交叉执行,这叫并发。所以说:JS是并发的。

既然是并发和并行,那就要用到同步。需要明白并发的同步比较简单,并行的同步比较复杂。

参考资料

https://www.jianshu.com/p/f11724034d50

https://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013868328689835ecd883d910145dfa8227b539725e5ed000

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!