Jenner's Blog

不变秃,也要变强!

0%

协程模型

历史背景

多线程编程

早起基于操作系统提供的线程的概念来实现并发编程,一个连接一个线程。在连接数目渐渐增多的时候,可扩展性很差,因为操作系统本身并不擅长处理大量的线程:任何一个线程的创建都需要设计到栈空间的分配、线程私有资源的创建和维护;同时大量的线程在操作系统层面被调度的时候,也很难保证调度策略的公平和有效。这也是C10K问题的由来。

解决方案

操作系统层面

通过在内核层面的IO多路复用角度触发,通过引入epoll等技术来允许一个单一的用户线程的IO调用可以管理海量的网络连接而完美地解决了传统的select调用层面的问题。
这个方案解决了一个线程处理海量网络IO的问题,但是对和网络无关的并发任务并没有多少帮助。同时,这种方案下的异步代码损失了可读性。

用户态线程/Green Thread

将逻辑线程的管理逻辑都放在操作系统的用户态来管理,而操作系统层面对这些机制一无所知。 运行在用户态的线程管理程序(往往是比较底层的基础库或者组件)自己负责这些线程状态的管理和调度。

协程

通过引入一个逻辑上抽象的概念来简化编程模型。某种程度上说,可以将一个协程看作是一段可以以非阻塞的方式高效执行的代码;而多个协程之间可以通过特定关键字或者语句的方式进行组合,以便程序员可以直接写出看起来是同步执行而实际上底层却是被异步调度执行的代码。这种方案既解决了程序员在异步编程上的心智负担,又解决了C10K问题。

Go的有栈协程

go语言被设计为天生支持协程,在代码中使用 go 关键字就能启动一个goroutinue,也就是协程。go的runtime两大主要职责:一个是垃圾回收,另一个就是调度协程。 go的协程调度模型可以称作GMP模型,其中:

  • G 表示 goroutinue,也就是协程
  • M 表示 os thread
  • P 表示调度上下文,每个M必须要先获取一个P,才能够开始执行G。

最初的调度模型中没有P的概念,在1.1版本后,go的调度器中引入了P,这么做主要是为了提升调度的性能,尽力的保证每个可运行的G都能够尽快运行。调度机制主要特点如下:

  • 每个P都有一个可运行的G队列
  • 另外有一个全局的可以运行G队列
  • M获取到P后,从P的运行队列中获取G来运行
  • 如果P本地的运行队列为空,则尝试从全局队列中获取G来运行
  • 如果全局的运行队列也为空,则尝试从其他P的运行队列中获取G

每个G运行的时间是由调度器来控制的,不会出现让某个G一直运行,而让其他G长时间的等待。如果M在运行G的时候发生了阻塞,比如block在某个系统调用,M则也会被阻塞住,P的本地队列会调度到其他M上执行。
P的数量可以通过GOMAXPROCS函数来设置,因为每个M对应一个P,因此这个函数也可以认为是设置了go程序最大的线程数量。(注意,M是可以大于P的,例如:一个M阻塞住了,那么正在运行的M数目还是和P的数目一样,这样总的M数目就大于P了。)

go语言中,协程的实现与操作系统多线程非常相似,依托于操作系统的多线程,在Runtime实现了一个协作式的调度器。go的系统调用是对底层系统调用的一个封装,被执行时,会将协程的上下文保存到堆栈中。

Rust的协程

Rust早期也是支持一个与go协程类似的绿色线程。在0.7以后,绿色线程就被删除了。因为Rust中Native thread与协程运行库的API很难统一。

无栈协程

Rust的无栈协程将协程上下文放到Generator生成的状态机中,保存在全局栈中,而不必为每个协程分配一个动态大小的栈。
无栈协程不需要与CPU相关的代码来分配栈内存,有了更好的跨平台能力。

await和async

在Rust1.36稳定版中加入了两个新的关键字:await和async。
引入这两个关键字使得Rust异步代码能够像同步顺序代码一样书写,避免手写poll方法的繁琐。

Reference

Rust语言的异步编程模型和协程支持
无栈协程 | Rust学习笔记
go和rust的协程模型

点击下方打赏按钮,获得支付宝二维码