协程
协程($Coroutine$)是一种轻量级的用户级线程,能够在程序执行中进行挂起和恢复。它与线程的不同之处在于,协程的切换并不依赖操作系统的调度器,而是由程序自身控制,因此协程的切换非常高效,且比线程更轻量。协程广泛用于需要高并发或异步编程的场景。
特性
- 轻量级:协程的上下文切换成本低。与操作系统调度的线程相比,协程不需要操作系统的干预,而是通过 程序内部 的控制进行切换。
- 挂起与恢复:协程可以在某个点 “挂起”,并且可以在之后的某个点 “恢复” 执行。挂起与恢复通常是由 协程自身控制 的,而不是操作系统的调度。
- 共享线程:协程通常运行在单个线程中。多个协程之间共享同一线程的资源,因此不会像线程那样进行线程切换,避免了线程之间的同步问题。
对称协程
- 定义:对称协程是一种协程调度方式,其中多个协程之间是平等的,协程之间的切换是由协程之间显式地协作来控制的。每个协程能够显式地调用其他协程,且每个协程在切换时都处于相同的 级别 ,没有 主协程 或 控制者。
- 特点
- 协程之间是对等的,互相切换时没有主次之分。
- 每个协程都可以决定何时挂起、何时恢复执行。
- 协程之间的切换由协程内部的调用控制,比如一个协程调用
yield
- 来挂起执行,并且可以在后续恢复执行。
非对称协程
- 定义:非对称协程是一种协程调度方式,其中 存在一个主协程(或称为调度器),并且其他协程是从主协程或调度器中被调用的。在非对称协程中,只有主协程 或调度器负责协程的调度和控制,其他协程则需要依赖主协程来恢复执行。
- 特点
- 只有一个“主协程”或调度器,负责控制协程的执行。
- 其他协程的执行由主协程来控制,协程不能主动控制调度。
- 主协程负责决定哪个协程执行,何时执行,何时挂起。
有栈协程和无栈协程
- 有栈协程:⽤ 独⽴的执⾏栈 来保存协程的上下⽂信息。当协程被挂起时,栈协程会保存当前执⾏状态(例如函数调⽤栈、局部变量等),并将控制权交还给调度器。当协程被恢复时,栈协程会将之前保存的执⾏状态恢复,从上次挂起的地⽅继续执⾏。类似于内核态线程的实现,不同协程间切换还是要切换对应的栈上下⽂,只是不⽤陷⼊内核⽽已。
- 无栈协程:它不需要独⽴的执⾏栈来保存协程的上下⽂信息,协程的上下⽂都放到 公共内存 中,当协程被挂起时,⽆栈协程会将协程的状态保存在堆上的数据结构中,并将控制权交还给调度器。当协程被恢复时,⽆栈协程会将之前保存的状态从堆中取出,并从上次挂起的地⽅继续执⾏。协程切换时,使⽤状态机来切换,就不⽤切换对应的上下⽂了,因为都在堆⾥的。⽐有栈协程都要轻量许多。
协程类的实现
$ucontext$
在正式编写协程类之前,⾸先需要学习⼀下 $Linux$ 下的
ucontext
族函数,ucontext
机制是 $GNU\ C$ 库提供的⼀组创建,保存,切换⽤户态执⾏上下⽂的 $API$,这是协程能够随时切换和恢复的关键。
- 常见的成员包括:
typedef struct ucontext_t {
struct ucontext_t *uc_link; // 当当前上下文运行终止时系统会恢复uc_link指向的上下文
sigset_t uc_sigmask; // 当前上下文的信号屏蔽掩码
stack_t uc_stack; // 当前上下文使用的栈空间数据
mcontext_t uc_mcontext; // 平台相关的上下文信息,包含寄存器的值
...
} ucontext_t;
uc_link:指向下一个激活的上下文对象。当当前上下文执行完毕后,程序会跳转到 uc_link 所指向的上下文。
如果该成员为NULL,表示当前上下文执行结束后不会再恢复其他上下文,程序会终止。
uc_sigmask:保存当前上下文的信号屏蔽掩码。该掩码决定了哪些信号会被阻塞。
uc_stack:表示当前上下文的栈空间(由 stack_t 结构体定义)。该栈用于存储函数调用的信息等,
只有通过 makecontext 创建的上下文才会使用栈空间。
uc_mcontext:该成员包含了平台相关的上下文内容,具体包括 CPU 寄存器的值。这个成员通常是一个结构体,
存储了与平台相关的处理器状态。
getcontext
函数
int getcontext(ucontext_t *ucp);
getcontext 会获取当前的执行上下文,并将其保存到 ucp 指向的 ucontext_t 结构体中。
该函数可以用于保存协程或线程的状态,以便之后恢复。
setcontext
函数
int setcontext(const ucontext_t *ucp);
setcontext 会恢复 ucp 所指向的上下文,即跳转到该上下文保存的状态。这个函数不会返回,
而是直接跳转到上下文中指定的函数执行。
makecontext
函数
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
makecontext 用于修改 ucp 指向的上下文,将其绑定到指定的函数 func 上。这个函数会将上下文的执行指令设置为从 func 开始执行。
ucp 是指向上下文的指针。必须先为 ucp 分配内存,并将其栈(uc_stack)设置好。
func 是上下文执行时调用的函数。
argc 和 ... 是可变参数,用于传递给 func 的参数。
重要说明:
必须在调用 makecontext 之前为 ucp->uc_stack 分配一块内存作为函数栈。
ucp->uc_link 可以设置为下一个上下文。函数执行完毕后,程序会跳转到 uc_link 指定的上下文。
如果不设置 uc_link,func 执行结束时必须显式调用 setcontext 或 swapcontext 恢复其他上下文,否则程序会跳出执行流,导致错误。
swapcontext
函数
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
swapcontext 是一个非常重要的函数,它会将当前上下文保存到 oucp 指向的 ucontext_t 结构体中,并恢复 ucp 指向的上下文。
与 setcontext 类似,swapcontext 也不会返回,而是直接跳转到 ucp 上下文执行。
使用场景:
swapcontext 在协程或线程切换时非常重要,通常用于保存当前上下文并切换到另一个上下文执行。
该函数通常由协程库实现上下文的切换,用于实现类似于协程调度的机制。在协程库中,主协程和子协程会通过 swapcontext 进行上下文切换。
- 案例
#include <ucontext.h>
#include <stdio.h>
void func1(void * arg) {
puts("1");
puts("11");
puts("111");
puts("1111");
}
int main() {
char stack[1024*128];
ucontext_t child,main;
getcontext(&child); //获取当前上下文(保存的是main的上下文,这里只是对child初始化,后面会修改)
child.uc_stack.ss_sp = stack;//指定栈空间
child.uc_stack.ss_size = sizeof(stack);//指定栈空间大小
child.uc_stack.ss_flags = 0;
child.uc_link = &main;//设置后继上下文
makecontext(&child,(void (*)(void))func1,0);//修改上下文指向func1函数
swapcontext(&main,&child);//切换到child上下文,保存当前上下文到main
puts("main");//如果设置了后继上下文,func1函数指向完后会返回此处
return 0;
}
- 在
ucontext_t
中,我们需要显式地为一个协程(或子上下文)指定栈空间,这是因为协程是从原始线程中分离出来的,必须为它们单独分配栈空间。否则,协程可能会因为使用共享栈空间导致栈溢出或数据破坏。 - 而
main
函数是程序的起始点,它所使用的栈空间已经由操作系统为其自动分配,因此不需要在上下文中显式指定栈空间。
$fiber$
- 本项目的使用的是 非对称模型,⼦协程只能与线程主协程进⾏切换,这种模型简单,⾮常容易理解.
- 具体实现上,使用线程局部变量
thread_local
来保存协程上下文,