一、并发与并行
1. 并发
并发(Concurrent):1个CPU交错执行2个任务。单核系统中,进程(或线程)通过时间片或出让控制权来实现任务切换,以达到“同时”运行多个程序的目的,实际上任何时刻都只有1个任务被执行。宏观上是“同时”执行,微观上是交错地顺序执行。
并发的特性
- 系统资源被多个进程(或线程)共享,造成程序结果不唯一
- 进程(或线程)结果的多变,导致进程(或线程)运行会出现不同的结果或偶发的异常
- 多个进程(或线程)间存在竞争资源产生的互斥关系,也存在协作完成一个整体任务产生的同步关系。
能否很好地解决多个进程(或线程)间的同步及互斥关系,将决定程序能否正常运行。
2. 并行
并行(Parallel):2个CPU分别同时各执行了1个任务。多核系统中,理想情况下,可以让多个进程(或线程)做到真正意义上的同时执行,它们之间不需要排队
通过下图中Erlang 之父 Joe Armstrong对并发与并行的说明,我们能清晰的区分并发与并行
二、进程、线程、协程
1. 进程
进程(Process):是操作系统进行资源分配和调度的一个独立单位。是一个具有特定功能的程序运行在一个数据集上的一次动态过程。是应用程序运行的载体。操作系统内核通过进程控制块(PCB,process control block)来感知进程。
进程的组成
- 程序:用于描述进程要完成的功能,是控制进程执行的指令集。(进程的执行)
- 数据集合:程序执行时所需的数据和工作空间。(进程的数据资源)
- 进程控制块(PCB):但基本包括进程标识符,当前状态,现场保护区,存储指针,占用资源表以及进程优先级等信息。它是进程存在的唯一标志。(进程的详细信息)
进程的切换
进程切换,就是把进程存放在处理器的寄存器中的中间数据存放到进程的私有堆栈中,从而把处理器的寄存器腾出来让其他进程使用。这个中间数据,就被称作该进程的上下文。进程的切换实质上就是被中止运行进程与待运行进程上下文的切换
进程的状态
就绪状态:进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。
运行状态:占有CPU,在CPU上执行。
阻塞状态:由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。
创建状态:进程正在被创建,系统为其初始化PCB,分配资源。
终止状态:进程正在从系统中撤销,回收进程的资源,撤销其PCB。
进程的通信方式
-
管道
管道可以看成是一种只存在内存中,不存在于任何文件系统中的特殊文件,支持普通的read、write 等函数。分为以下两种管道
匿名管道(pipe):
半双工(即数据只能在一个方向上流动),具有固定的读端和写端,只能用于具有亲缘关系的进程之间的通信。
命名管道(FIFO):
半双工,有自己的名字和访问权限的限制,就像一个文件一样,它可以用于不相关进程间的通信,进程通过使用命名管道的名字获得管道。管道的特点:写满时,不能再写;读空时,不能再读
使用场景:适合两个进程间发送非常短小的、频率很高的消息。 -
消息队列
由消息组成的链表,存放在内核中并由消息队列标识符标识。消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。 -
信号
信号是一种非常短的消息,短到只有一个数字。一个进程可以向另外一个进程或者另外一组进程发送信号消息,通知目标进程执行特定的代码 -
信号量
用于实现进程间的互斥与同步,而不用于存储进程间通信数据,是一种保证共享资源有序访问的工具。 -
共享内存
允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程。
共享内存将保持到通信完毕为止,不会频繁解除内存映射或重建内存共享区域,因此共享内存的通信方式效率非常高。
使用场景:适合多进程间共享的、非常庞大的、读写操作频率很高的数据通信。 -
网络Socket
网络环境中进程间通信的API。与其他通信机制不同的是,它可用于不同机器间的进程通信。
使用场景:适用于分布式开发
2. 线程
线程(thread):是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
线程的分类
线程的实现可以分为两类:
1)用户级线程(User-Level Thread)
由进程负责调度管理,不依赖于操作系统内核
优点:
- 线程位于用户空间(即不需要模式切换)。
- 完全控制线程调度器(例如:网站服务器)。
- 独立于操作系统(线程可以在不支持它们的操作系统上运行)。
- 运行时系统(run time system)可以切换用户空间中的本地阻塞线程(例如:等待另一个线程完成)。
缺点:
- 系统调度中,对一个线程的阻塞将会导致整个进程阻塞(例如:当一个线程因 I/O 而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会)。
- 网站服务器中,一个页面的错误将导致整个进程阻塞。
- 非真正意义的线程并行(一个进程安排在单个CPU上)。
- 不存在时钟中断(例如,如果用户线程是非抢占式的,将无法被“进程调度”以round-robin的调度算法调用,因为round-robin调度算法中限制了cpu时间片)。
2)内核级线程(Kernel-Level Thread)
由操作系统支持和管理
优点:
- 实现了真正意义上的线程并行。
- 不需要运行时系统的参与。
缺点:
频繁的模式切换导致内核开支。
线程的同步
当多个线程同时读写同一份共享资源的时候,可能会引起冲突,这时候,我们需要引入线程“同步”机制。线程同步是为了防止多个线程同时访问同一个数据对象时,对数据造成破坏。线程的同步是保证多线程安全访问资源的一种手段。
主要通过临界区(Critical Section)、互斥对象(Mutex)的机制来实现互斥控制,在Java中分别对应synchornized及对象锁;通过信号量(Semaphore)、事件对象(Event)以通知的方式进行同步控制,在Java中分别对应wait()、notify()等方法。
也可以通过写时复制(Copy On Write)的无锁方式来实现线程的同步,即在每个线程中拷贝一份共享资源的副本。
线程的出现,是为了分离进程的两个功能:资源分配和系统调度。让更细粒度、更轻量的线程来承担调度,减轻调度带来的开销。但线程还是不够轻量,因为调度是在内核空间进行的,每次线程切换都需要陷入内核,这个开销还是不可忽视的。协程则是把调度逻辑在用户空间里实现,通过自己(编译器运行时系统/程序员)模拟控制权的交接,来达到更加细粒度的控制。
在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3种线程对应模型,也就是:1:1,1:N,M:N。
- 1:1:一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文切换很慢,切换效率很低。
- N:1:多个(N)用户线程始终在一个内核线程上跑,context上下文切换很快,但是无法真正的利用多核。
- M:N:多个用户线程在多个内核线程上跑,这个可以集齐上面两者的优势,既能快速切换上下文,也能利用多核的优势
3. 协程
协程(Coroutine):是一种用户级的轻量线程,拥有自己独立的栈和共享的堆,共享堆,不共享栈。协程由程序员在协程的代码里显示调度。进程、线程是操作系统级别的概念,而协程是编译器级别的,协程间切换只需要保存任务的上下文,没有内核的开销。
协程的优势:
- 内存占用少
- 上下文切换代价小
4. goroutine
Goroutine基本概念:
- goroutine是Go语言运行库的功能,不是操作系统提供的功能,goroutine不是用线程实现的,而是go语言实现的用户态线程。
- goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行。
- goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有goroutine阻塞,该线程的其他goroutine也可以被runtime调度,转移到其他可运行的线程上。这更像是多线程和协程的综合体,能最大限度提升执行效率,发挥多核处理能力。
Goroutine特点:
- 占用内存更小(几kb)
- 调度更灵活(runtime调度)
参考文档:
https://zhuanlan.zhihu.com/p/137339439
https://zhuanlan.zhihu.com/p/260830550
https://zhuanlan.zhihu.com/p/51194025
https://www.cnblogs.com/LUO77/p/5816326.html
https://blog.csdn.net/cafucwxy/article/details/78453430
https://blog.csdn.net/zhaohong_bo/article/details/89552188
https://www.coder55.com/article/11579
http://www.sizeofvoid.net/goroutine-under-the-hood/
https://zhuanlan.zhihu.com/p/68299348
网友评论