# coroutine

# 进程、线程、协程
线程是CPU调度的最小单位, 进程是资源分配的最小单位。对于线程和进程，我们可以这么理解：

* 当进程只有一个线程时，可以认为进程就等于线程。
* 当进程拥有多个线程时，这些线程会共享相同的虚拟内存和全局变量等资源。这些资源在上下文切换时是不需要修改的。
* 线程也有自己的私有数据，比如栈和寄存器等，这些在上下文切换时也是需要保存的。

协程作为用户态线程，也是轻量级的线程，用来解决高并发场景下线程切换的资源开销。协程跟线程是有区别的

* 线程/进程是内核进行调度，有 CPU 时间片的概念，进行 抢占式调度（有多种调度算法）
* 协程对内核是透明的，也就是系统并不知道有协程的存在，是完全由用户自己的程序进行调度的，因为是由用户程序自己控制，那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程，通常只能进行 协作式调度，需要协程自己主动把控制权转让出去之后，其他协程才能被执行到。


# GO协程的设计与原理

## GMP模型

* G（Goroutine）：用户态协程，逻辑上的计算单元
* M（Machine/OS Thread）：绑定操作系统线程，实际执行G
* P（Processor）：调度器抽象，用于调度G给M

## go func
‘’‘go
func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Print(i)
        }()
    }
    time.Sleep(1 * time.Second)
}
’‘’
上述代码的运行结果为：

![](./pics/goroutine.jpg)

这段代码中开启了十个线程，可以看到并不是按顺序打印且每次不一样，这是因为这十个线程的调度时间并不固定，只有等到被调度执行的时候才会实际打印。

每个我们开启的协程都是一个计算任务，这些任务会被提交给go的**runtime**。若有多个计算任务，这些计算任务会被先暂存起来，一般的做法是放到内存的队列中等待被执行。

消费端则是一个go runtime维护的一个调度循环。调度循环简单来说，就是不断从队列中消费计算任务并执行。这里本质上就是一个**生产者-消费者**模型，实现了用户任务与调度器的解耦。

## 调度策略

为了避免多个M访问同一个队列，我们把全局队列分为多个多个本地队列，这个本地队列由P来管理。这样一来，每个M只需要去先找到一个P结构，和P结构绑定，然后执行P本地队列里的G。

![](./pics/gmp.jpg)

上图中各个模块的作用如下：

* 全局队列：存放等待运行G
* P的本地队列：和全局队列类似，存放的也是等待运行的G，存放数量上限256个。**新建G时，G优先加入到P的本地队列，如果队列满了，则会把本地队列中的一半G移动到全局队列**
* P列表：所有的P都在程序启动时创建，保存在数组中，最多有GOMAXPROCS个，可通过runtime.GOMAXPROCS(N)修改，N表示设置的个数。
* M：每个M代表一个内核线程，操作系统调度器负责把内核线程分配到CPU的核心上执行。

**个人感觉GMP中的M就像线程池中的线程，Go runtime会动态创建和复用M，避免频繁线程切换或销毁，提升并发性能。**区别在于线程池中的线程在等待IO的时候会阻塞，但协程模型中，这个线程会在调度器的调度下运行另一个任务，IO完成后再唤醒，且任务切换的开销远小于切换线程。

调度策略： 调度器核心思想是尽可能避免频繁的创建、销毁线程，对线程进行复用以提高效率。
1. work stealing机制（窃取式）：当本线程无G可运行时，从其他线程绑定的P窃取G，而不是直接销毁线程。
2. hand off机制：当本线程M1因为G进行的系统调用阻塞时，线程释放绑定的P，把P转移给其他空闲的M0执行。
3. 抢占：一个goroutine最多占用CPU10ms，防止其他goroutine等待太久得不到执行被“饿死”。

优势： 

* goroutine是用户态线程，其创建和切换都在用户代码中完成而无需进入操作系统内核，所以其开销要远远小于系统线程的创建和切换；
* goroutine启动时默认栈大小只有2k，这在多数情况下已经够用了，即使不够用，goroutine的栈也会自动扩大，同时，如果栈太大了过于浪费它还能自动收缩，这样既没有栈溢出的风险，也不会造成栈内存空间的大量浪费。

# rust协程：tokio
与 Go 等语言的有栈协程不同，Rust 的 async/await 实现的是无栈协程。这种设计在内存占用和任务切换开销上通常更具优势。

在 Rust 中，一个 async 代码块或 async fn 函数在编译后会生成一个实现了 Future trait 的状态机结构体。这个 Future 本身可以存在于栈上。当我们调用 tokio::spawn 将其作为一个任务交给调度器时，Tokio 会将其**装箱（Box）**并放到堆上，以便在不同线程间安全地移动和管理。随后，这个被包装的任务被推入异步任务队列，等待调度器线程执行。

## 协程运行机制
在rust中，`async fn`用来定义一个异步函数，被编译成状态机结构体Future，`tokio::spawn`会把Future推入到调度器的任务队列，实际线程从队列中取任务，调用`Future::poll`方法，返回`Poll::Ready`则任务完成。如果 poll 方法返回 `Poll::Pending`，这表明任务在某个 .await 处遇到了一个尚未完成的异步操作，此时任务需要等待。这并非传统意义上的“线程阻塞”，而是一个**挂起点**。

此时，任务会：

1. 向执行器（Executor）注册一个 Waker。这个 Waker 知道如何唤醒当前任务。

2. 任务被挂起，执行它的工作线程则会立即丢下它，去处理本地队列中的下一个就绪任务，线程本身从不阻塞。

当等待的事件完成时（例如，网络数据到达），事件源会调用之前注册的 Waker 的 wake() 方法。被唤醒的任务会被重新放回调度器的就绪队列中，等待下一次被线程轮询。
状态机的转换过程如下图：

![](./pics/future.jpg)

## 调度器
Tokio采用多线程、work-stealing调度策略：
* 每个线程都有一个本地任务队列；
* 在 Tokio 工作线程内部调用 spawn 时，优先尝试将这个新任务放入当前工作线程自己的本地运行队列中，增强数据局部性。
* 当本地任务队列已满时或者从 Tokio 运行时外部调用 spawn 时，会尝试将任务放入全局任务队列中；
* 空闲线程可以从其他线程（包括全局队列）“偷”任务；
* 每个任务的 Future 是 Send + 'static，所以可以在不同线程间移动执行。

## 适用场景
* I/O 密集型（I/O-Bound）

**Tokio 的异步模型最核心的优势在于处理 I/O 密集型任务。**例如网络请求、数据库连接、文件读写等。

原因在于：对于这类任务，程序大部分时间都在等待外部资源（如网络、磁盘）的响应。在传统的同步阻塞模型中，等待会使整个线程挂起，浪费 CPU 资源。而在 Tokio 中，当一个任务等待 I/O 时，它会主动让出（.await）CPU，使得执行线程可以去处理成百上千个其他并发任务。这种“在等待时做别的事”的模式极大地提高了单线程的利用率和整个系统的吞吐能力。

* CPU 密集型（CPU-Bound）
  
对于需要长时间进行密集计算的任务，不应该直接在 Tokio 的主异步上下文中运行。

原因在于：这样的任务会长时间占用线程的 CPU 时间，并且从不“让出”（因为没有 .await I/O 操作）。这会导致它所在的线程无法去轮询其他任务，造成**调度器饥饿**，使得该线程上的所有其他异步任务都得不到执行，严重影响响应性。

对于这类计算密集型任务，可以派发到专门的阻塞线程池中去执行。

