go和rust的协程模型

协程

协程是一种可以调度的计算单元,它和线程有很多相似的地方:可以被挂起和恢复,有自己的运行上下文。比较大的一个不同之处在于:协程的调度发生在用户态,由用户态程序来控制和管理,而线程则是由OS直接调度的。多个协程可以都在一个线程上运行,他们的运行过程是并发的,但并不能真正的并行执行。由协程调度程序来切换各个协程的运行,使它们表现得像同时在执行。

简单的说,协程就是轻量级的线程,使用协程的主要目的就和使用多线程的目的一样,提高程序的性能,增加cpu的使用率。

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了。)

rust 中的协程模型

rust语言本身没有支持协程,它没有runtime,也就不能做调度,gc这样的事情。不过rust提供了一些定义好的trait,如Future,可以用来实现协程。在rust中使用协程主要是通过tokio 包,tokio是一个用于异步编程的包,它的实现包括一个task系统,一个runtime,以及异步编码的一些基本库。

在tokio中,可以认为task就是协程,由tokio runtime调度执行。task有两个非常重要的特点:

  • task不会阻塞,它是全异步的
  • task的执行不会被打断,也就是如果task不主动让出,是不会被切换走的

如果一个task中依赖了某个底层资源,runtime执行这个task的时候,根据底层资源是否ready,可以分为两种情况处理:1,底层资源已经ready,那么task可以立即返回需要的结果;2,底层资源还没有准备好,那么task返回NotReady,并再次进入调度队列。task系统此时会记录下这个task和相应底层资源的关系,等到底层资源ready后,会再次将task交给runtime来执行。

如果task中没有依赖底层资源,runtime执行这个task的时候,会立即返回结果。需要注意的是,如果task中的计算逻辑过重,占用cpu时间过长,会影响到其他task的执行。因为tokio的runtime没有给task分配执行时间片,而是会一直将这个task执行完成,这个过程中,其他的task可能会长时间得不到执行,这是一种非预期的情形。这也是为什么task中不能写太重的计算逻辑的原因。

在使用tokio时,可以选择使用哪种类型的runtime,主要有两种:local thread 和 threadpool,也就是单线程和多线程的区别。

对比

比较go和rust中的协程实现来看,go的协程模型相对来说更好理解,可以类比OS调度线程的机制。由于goroutinue是可以阻塞的,只要某个goroutinue阻塞了,就将剩余可运行的goroutinue调度到其他线程上执行。

而rust的tokio则是一个异步的执行框架,task是异步非阻塞的,并且task的执行不可抢占。它和nginx,libevent,redis的运行机制更相似,但多了task这个概念,整个调度的机制显得更复杂。

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页