在了解无栈协程前,我们先看看有栈的情况,其实这就是普通的函数调用。

有栈协程

在c语言中,函数调用和返回过程中会使用栈上数据进行恢复调用前状态。程序总体上是同步顺序运行的,他们都使用同一个栈。但是随着不断地函数调用,这个栈会越来越深,所以会带来极大的开销。

而有栈协程中的有栈到底是什么意思,这里其实是约定成俗想表达对于每个新创建的协程来说:他们都独立运行与一块新的栈,这块栈是从堆(基于mmap维护了整个内存管理)上面申请的,没用共用系统栈,那么这个协程的生命周期和上下文都能够被完整保存,可以被任意时间和任意线程独立执行

go 创建协程

在golang中可以使用go关键字创建协程,在创建后整个函数与当前的主线程没有瓜葛,相当于单独开辟一个栈给这个程序使用。

go func(){  
fmt.Println("this is a goroutine print!")
}

这里就不深入了解了,主要是需要学习无栈协程。

无栈协程

协程的实现需要保存上下文,但是我又不能依赖栈,因为这样可能会带来其他开销,那我们该如何做到呢?带着这个疑问我们看看rust魔法。

首先看看goland和rust关于协程的语法区别:

func main(){  
go fun(){
fmt.Println("ready to sleep!")
time.Sleep(8 * time.Second)
//会暂停当前函数执行,给其他协程继续执行
//等待睡眠时间到后重新调度后继续从当前位置向下执行
fmt.Println("hello world 1!")
}
}
tokio::spawn(async {  
println!("ready to sleep");
tokio::time::sleep(time::Duration::from_secs(2)).await;
//注意: 一定要加await!
//当前函数会在这里暂停,等待睡眠时间到后继续恢复执行
println!("hello world!");
});
  • 由于rust没有自带的运行时,所有协程的调度、执行、切换都需要依赖三方实现,比较好的就是tokio
  • 编译器只干了一件事情: 在有await语句的地方检测是否ready,否则挂起函数,等待下次运行

既然rust不像golang那样有单独的栈,那他怎么实现上下文保存和栈的重入呢?

在了解rust通过await实现协程(特别强调await)前来一起看看什么叫做状态机吧。

状态机

理解协程:在特定的位置暂停(注意这不是阻塞),接着去执行其他协程,然后被唤醒后重新在当前恢复

golang使用过独立的栈做到,而rust协程是通过状态机实现。这一点区别很重要。

  1. 重点: rust编译器会将带有.await的代码快转换为一个enum 状态机。
  2. 对于每个await的代码都实现为一个enum的分支
  3. 每次协程的暂停和恢复只是进入不同的代码分支罢了

虽然没有了额外的占的开销,但实际上编译器会生成很多指令和分支来支持这个状态机

不过相比需要额外的栈内存来实现协程,这种方式已经非常棒了