异步、并发、协程原理

进程、线程、协程的特点及区别

支持协程的语言

适合使用协程的场景

协程与异步和并发的联系

一、异步

应用程序和内核

Linux将虚拟空间分为用户空间(应用程序使用)和内核空间(内核使用),两者隔离、相互独立

内核权限最高,可访问受保护的内存空间,底层硬件设备。应用程序得通过系统调用(内核提供的接口)来间接访问或操作

常见IO模型就基于应用程序和内核之间的交互提出

例 网络IO read操作

请求数据会先拷贝到系统内核的缓冲区(内核空间),再从操作系统的内核缓冲区拷贝到应用程序的地址空间(用户空间)

从内核空间将数据拷贝到用户空间过程中,经历两个阶段:

  • 等待数据准备
  • 拷贝数据

正因为有了这两个阶段,才提出了各种网络I/O模型

Unix/Linux的体系架构

同步和异步

应用程序与内核的交互方式

  • 同步

应用发起I/O请求后要等待或者轮询内核I/O操作完成后才能继续执行

  • 异步

应用发起I/O请求后仍继续执行,内核I/O操作完成后会通知应用程序,或者调用应用程序注册的回调函数

阻塞和非阻塞

应用调用内核IO操作的方式

  • 阻塞

指I/O操作彻底完成后才返回到用户空间

  • 非阻塞

I/O操作被调用后立即返回给用户一个状态值,无需等到I/O操作彻底完成

常见的网络I/O模型大概有四种:

  • 同步阻塞IO(Blocking IO)
  • 同步非阻塞IO(Non-blocking IO)
  • IO多路复用(IO Multiplexing)
  • 异步IO(Asynchronous IO)

IO多路复用模型

select、poll、epoll 可同时监控多个流的 I/O 事件,空闲的时候,当前线程阻塞掉

有一或多个流有I/O事件时,从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,避免大量无用操作

“多路”指的是多个网络连接,“复用”指的是复用同一个线程

多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗)

IO多路复用是异步阻塞的 ?

二、并发

并发在os 中,指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行

并发和并行的区别

  • 并发(concurrency)

逻辑上具备同时处理多个任务的能力

  • 并行(parallesim)

物理上在同一时刻执行多个并发任务,依赖多核处理器等物理设备

多线程或多进程是并行的基本条件,但单线程也可用协程做到并发

通常

多进程实现分布式和负载平衡,减轻单进程垃圾回收压力

多线程抢夺更多的处理器资源

协程提高处理器时间片利用率

现代系统中,多核cpu可以同时运行多个不同的进程或者线程

所以并发程序可以是并行的,也可以不是

三、协程

1、线程模型

计算机结构两种线程模型:用户/内核级线程

用户级线程

应用在 os 提供的单个控制流的基础上,通过在某些控制点(比如系统调用)上分离出一些虚拟的控制流,模拟多个控制流的行为

由于应用程序对指令流的控制能力相对较弱,所以,用户级线程之间的切换往往受线程本身行为以及线程控制点选择的影响,线程是否能公平地获得处理器时间取决于这些线程的代码特征

而且,支持用户级线程的应用程序代码很难做到跨平台移植,以及对于多线程模型的透明优势

线程切换效率高,不涉及系统内核模式和用户模式之间的切换

应用可以采用适合自己特点的线程选择算法,根据应用程序的逻辑来定义线程的优先级,线程数量很大时,这一优势尤为明显。但同样会增加应用程序代码的复杂性

一些软件包(如POSIX Threads 或Pthreads 库)可以减轻程序员的负担

内核级线程

os 提供的线程语义,由于 os 对指令流有完全的控制能力,甚至可以通过硬件中断来强迫一个进程或线程暂停执行,以便把处理器时间移交给其他的进程或线程,所以,内核级线程有可能应用各种算法来分配处理器时间

线程可以有优先级,高优先级的线程被优先执行,它们可以抢占正在执行的低优先级线程

在支持线程语义的操作系统中,处理器的时间通常是按线程而非进程来分配,因此,系统有必要维护一个全局的线程表,在线程表中记录每个线程的寄存器、状态以及其他一些信息

然后,系统在适当的时候挂起一个正在执行的线程,选择一个新的线程在当前处理器上继续执行

“适当的时候”如

  • 线程执行系统调用时,例sleep放弃执行权的系统函数,或wait、select 这样的阻塞函数
  • 硬中断(interrupt)或异常(exception)
  • 线程终止时,等等

这些时间点的执行代码可能分布在 os 的不同位置,所以,os 线程调度(thread scheduling)往往比较复杂,其代码通常分布在内核模块的各处

内核级线程好处

应用无须考虑是否要在适当的时候把控制权交给其他的线程,不必担心自己霸占处理器而导致其他线程得不到处理器时间,只要按照正常的指令流来实现自己的逻辑即可,内核会妥善地处理好线程之间共享处理器的资源分配问题

代价

对应用程序的便利也是有代价的(上下文切换),线程切换在内核模式下完成,用户模式下运行的一个线程被切换出去,以及下次轮到它的时候再被切换进来,涉及两次模式切换:从用户模式切换到内核模式,再从内核模式切换回用户模式

Intel处理器大致需要几百个甚至上千个处理器指令周期 模式切换的开销相对于现代操作系统的线程调度周期(通常几十毫秒)的比例正在减小,所以,这部分开销是完全可以接受的

除线程切换开销外,线程的创建和删除也是一个重要的考虑指标,线程数量较多时这部分开销是相当可观

进程内建立起一个线程的执行环境,例如,分配线程本身的数据结构和它的调用栈,完成这些数据结构的初始化工作,以及完成与系统环境相关的一些初始化工作线程数量较多时,伴随而来的线程切换开销也必然随之增加 所以应用程序或系统进程需要的线程数量可能比较多时,通常用线程池作为一种优化措施,降低创建和删除线程以及线程频繁切换而带来的开销

支持内核级线程的系统环境中,进程可以容纳多个线程,导致了多线程程序设计(multithreaded programming)模型

由于多个线程在同一个进程环境中,它们共享了几乎所有的资源,所以,线程之间的通信要方便和高效得多,这往往是进程间通信(IPC,Inter-Process Communication)所无法比拟的,但是,这种便利性也很容易使线程之间因同步不正确而导致数据被破坏,而且,这种错误存在不确定性,因而相对来说难以发现和调试

2 、协同式和抢占式

桌面 os( Windows 3.2, Mac OS 9 等)从协同式调度过渡到抢占式多任务系统调度方式并无高下,取决于应用场景

抢占式系统允许操作系统剥夺进程执行权限,抢占控制流,适合服务器和图形操作系统,调度器可以优先保证对用户交互和网络事件的快速响应

协同式调度则等到进程时间片用完或系统调用时转移执行权限,适合实时或分时等等对运行时间有保障的系统

抢占式系统依赖CPU硬件支持。 因为调度器需要“剥夺”进程的执行权,调度器需要比普通进程高的运行权限(RING),否则任何“流氓(rogue)”进程都可以去剥夺其他进程了

协同式多任务适用于那些没有处理器权限支持的场景,包含资源受限的嵌入式系统和实时系统

这些系统中程序均以协程的方式运行

调度器负责控制流的让出和恢复。通过协程的模型,无需硬件支持,就可以在“简陋”的处理器上实现多任务的系统。许多智能设备,如运动手环,基于硬件限制,都是采用协同调度的架构

协程概念

可理解为用户态线程,协作而非抢占进行切换,轻量级多任务模型

相对于进程或者线程,协程所有操作都可在用户态完成,创建和切换消耗更低

总的来说协程为协同任务提供了一种运行时抽象,这种抽象非常适合于协同多任务调度和数据流处理

编程角度上协程本质上就是控制流的主动让出(yield)和恢复(resume)机制,迭代器常被用来实现协程

大部分语言实现的协程中都有yield关键字,如Python、PHP、Lua

特殊如Go用通道来通信

进程、线程、协程的特点及区别

进程

  • 资源分配最小单位
  • 进程间不共享内存,每个进程拥有自己独立的内存
  • 进程间通过信号、信号量、共享内存、管道、队列等来通信
  • 新开进程开销大,cpu切换进程成本也大
  • 由操作系统调度
  • 多进程方式比多线程更加稳定

线程

  • 程序执行流最小单位
  • 线程来自于进程的,一个进程下面可以开多个线程
  • 每个线程都有自己一个栈,不共享栈,但多个线程能共享同一个属于进程的堆
  • 线程因为是在同一个进程内的,可以共享内存
  • 线程也是由操作系统调度,线程是CPU调度的最小单位
  • 新开线程开销小于进程,cpu在切换线程成本也小于进程
  • 某个线程发生致命错误会导致整个进程崩溃
  • 线程间读写变量存在锁的问题处理起来相对麻烦

协程

  • 对os来说只有进程和线程,协程控制由应用程序显式调度,非抢占式的
  • 协程的执行最终靠的还是线程,应用程序来调度协程选择合适的线程来获取执行权
  • 切换非常快,成本低。一般占用栈大小远小于线程(协程KB级别,线程MB级别),所 以可以开更多的协程
  • 协程比线程更轻量级

堆区(heap)?栈区(stack)?

栈依附于线程

栈由系统自动分配,堆在程序启动时和在程序运行过程中都可以由程序显式申请分配

栈由系统自动释放,堆可自由存放数据,由程序或者垃圾回收器在任意时候释放

栈的存取速度高于堆(寄存器放栈指针)

支持协程的语言

Simula Modula-2 C# Lua Go JavaScript(ECMA-262 6th Edition) Python Ruby Erlang PHP(PHP5.5+) …

C

C标准库setjmp/longjmp 函数可以用来实现一种协程

go

gorouting

Go原生语言级并发,并发最小逻辑单元goroutine

goroutine是Go语言提供的一种用户态线程,跑在内核级线程上

很多goroutine都是跑在同一个内核线程上的时候,需要调度器 (scheduler)来维护这些goroutine,确保所有的goroutine都使用cpu,并且是尽可能公平的使用cpu资源

Go scheduler实现了M:N的模式(多个goroutine在多个内核线程上跑)

goroutine让Go低成本地具有了高并发运算能力。goroutine通过通道(channel)来通信

注意:goroutine的实现并不完全是传统意义上的协程。在协程阻塞的时候(cpu计算或者文件IO等),多个goroutine会变成多线程的方式执行

1
2
3
4
5
6
7
func main() {
 for i := 0; i < 100; i++ {
 go func() { // 启动一个goroutine
 fmt.Println(i)
 }()
 }
}

Python

基于Generator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def grep(pattern):
 while True:
 line = (yield)
 if pattern in line:
 print(line)


search = grep('coroutine')
next(search) # 启动一个协程
search.send("send sha ne")

Lua

Lua 中的协同是一协作的多线程,每一个协同等同于一个线程,yield-resume可以实现在线程中切换

与真正的多线程不同的是,协同是非抢占式的

当程序运行到 yield 的时候,使用协程将上下文环境记录住,然后将程序操作权归还到主函数,当主函数调用 resume 的时候,会重新唤起协程,读取yield记录的上下文。这样形成了程序语言级别的多协程操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
co = coroutine.create( -- 创建coroutine
 function(i)
 print(i);
 end
)

coroutine.resume(co, 1) -- 唤醒coroutine
print(coroutine.status(co)) -- 查看coroutine的状态

co = coroutine.wrap(
 function(i)
 print(i);
 end
)

co(1)

co2 = coroutine.create(
 function()
 for i=1,10 do
 print(i)
 if i == 3 then
 print(coroutine.status(co2)) 
 print(coroutine.running()) -- 返回正在跑的coroutine
 end
 coroutine.yield() -- 挂起coroutine
 end
 end
)

PHP

PHP5.5 加入了对迭代生成器和协程的支持

也基于Generator,Generator可以视为一种“可中断”的函数,而 yield 构成了一系列的“中断点”

PHP 协程没有resume关键字,而是“在使用的时候唤起”协程

1
2
3
4
5
6
7
8
9
function xrange($start, $end, $step = 1) {
 for ($i = $start; $i <= $end; $i += $step) {
 yield $i;
 }
}

foreach (xrange(1, 1000000) as $num) { // xrange返回的是一个Generator对象
 echo $num, "\n";
}

Swoole

Swoole 2.0内置协程(Coroutine)的能力,提供了具备协程能力IO接口(统一在命名空间Swoole\Coroutine*)

基于setjmp、longjmp实现,在进行协程切换时会自动保存Zend VM的内存状态(主要是EG全局内存和vm stack)

由于swoole是在底层封装了协程,所以对比传统的php层协程框架,开发者不需要使用yield关键词来标识一个协程IO操作,所以不再需要对yield的语义进行深入理解以及对每一级的调用都修改为yield

适合使用协程的场景

IO密集型场景,提高并发性,比如请求接口、Mysql、Redis等的操作

替代异步回调的代码风格

无感知用同步的代码编写方式达到异步IO的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护

相比普通异步回调程序,协程会多增加额外的内存占用和一些 CPU 开销

协程与异步和并发的联系

协程与异步

并不是说替换异步,协程一样可以利用异步实现高并发

协程与并发

协程要利用多核优势就需要比如通过调度器来实现多协程在多线程上运行,这时也就具有了并行的特性。如果多协程运行在单线程或单进程上也就只能说具有并发特性