文章目录
1.协程的库
1.golang的libgo(使用非常简单,C++实现)
2.腾讯的libco(非常好用的,C++实现)
3.ntyco(纯C的代码写)–wangbojing
4.今一些具备协程语义的语言,比较重量级的如C#、erlang、golang,以及轻量级的python、lua、javascript、ruby,还有函数式的scala、scheme等
2.为什么会有协程?
1.协程就是轻量级线程,而普通线程切换成本高,性能类似io异步操作
2.异步化,起初人们喜欢同步编程,然后发现有一堆线程因为I/O卡在那里,并发上不去,资源严重浪费
3.线程安全,任何挂起的函数可以在主线程调用。
4.协程作为有异步性能,同步的代码逻辑。来方便编程人员对 IO 操作的
组件
5.总结
3.协程解决了什么问题?
1)同步的方式实现异步的效率,建立1000条连接,同步需要5400毫秒,异步需要850毫秒
2)同步代码(epoll_wait和send、wait在同意不流程里面)
3)异步代码(epoll_wait检测io可读写之后放到另外一个线程里面)
4)异步io与io异步操作
io异步操作概念:就是上面异步的代码与操作讲解(多线程异步)
异步io概念:例如AIO,纯异步,有数据直接回调数据
4.协程有栈和无栈的区别
1)有栈:每一个协程,有独立的栈,但是有栈的性能更高
优点:实现容易,性能更高
缺点:栈利用率不好
特点:Stackfull是真正的基于栈的重入,可以从某个嵌套的调用点上恢复执行。Stackfull协程在切换的时候,需要把当前的栈保存起来,以便在恢复的时候再次恢复执行,而stackless的则不需要。
2)无栈:共享栈(stackless),对内存利用率更高,但是更加复杂
优点:栈利用率高
缺点:性能可能比前者低
特点:Stackless类型不保存调用栈以及寄存器等信息,不属于真正的重入,因此一些局部变量都是无法使用的。
5.协程的切换怎么实现(Longjmp/setjmp,utext,汇编三种方法)
1.声明(汇编实现)
2.实际让出CPU资源的代码(yield)
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);
#ifdef __i386__
__asm__ (
" .text \n"
" .p2align 2,,3 \n"
".globl _switch \n"
"_switch: \n"
"__switch: \n"
"movl 8(%esp), %edx # fs->%edx \n"
"movl %esp, 0(%edx) # save esp \n"
"movl %ebp, 4(%edx) # save ebp \n"
"movl (%esp), %eax # save eip \n"
"movl %eax, 8(%edx) \n"
"movl %ebx, 12(%edx) # save ebx,esi,edi \n"
"movl %esi, 16(%edx) \n"
"movl %edi, 20(%edx) \n"
"movl 4(%esp), %edx # ts->%edx \n"
"movl 20(%edx), %edi # restore ebx,esi,edi \n"
"movl 16(%edx), %esi \n"
"movl 12(%edx), %ebx \n"
"movl 0(%edx), %esp # restore esp \n"
"movl 4(%edx), %ebp # restore ebp \n"
"movl 8(%edx), %eax # restore eip \n"
"movl %eax, (%esp) \n"
"ret \n"
);
#elif defined(__x86_64__)
__asm__ (
" .text \n"
" .p2align 4,,15 \n"
".globl _switch \n"
".globl __switch \n"
"_switch: \n"
"__switch: \n"
" movq %rsp, 0(%rsi) # save stack_pointer \n"
" movq %rbp, 8(%rsi) # save frame_pointer \n"
" movq (%rsp), %rax # save insn_pointer \n"
" movq %rax, 16(%rsi) \n"
" movq %rbx, 24(%rsi) # save rbx,r12-r15 \n"
" movq %r12, 32(%rsi) \n"
" movq %r13, 40(%rsi) \n"
" movq %r14, 48(%rsi) \n"
" movq %r15, 56(%rsi) \n"
" movq 56(%rdi), %r15 \n"
" movq 48(%rdi), %r14 \n"
" movq 40(%rdi), %r13 # restore rbx,r12-r15 \n"
" movq 32(%rdi), %r12 \n"
" movq 24(%rdi), %rbx \n"
" movq 8(%rdi), %rbp # restore frame_pointer \n"
" movq 0(%rdi), %rsp # restore stack_pointer \n"
" movq 16(%rdi), %rax # restore insn_pointer \n"
" movq %rax, (%rsp) \n"
" ret \n"
);
#endif
6.协程的启动
1.协程的创建
2.创建后如何加入到就绪队列
3.协程入口函数怎么调用
eip指针指向入口函数,比如入口函数func的地址指向eip
栈指针esp–>的首地址保存起来,放在数组也是ok的(eip指向栈)
7.协程定义
1.上下文context,用来切换用,用来存cpu寄存器的值
2.每个协程的栈空间stack,栈就是协程内部进行函数调用用来压栈的栈指针要用到
3.协程栈的大小size
4.协程的入口函数func
5.func的入口函数arg
6.协程处于wait\sleep\ready的状态,这三个都是集合元素,可分别为红黑树、红黑树、队列
7.exit退出
8、协程调度器是怎么实现(协程调度器schedule)
1.找到当前协程运行的协程curr_coroutine
2.当前用到的共享栈,共享栈的化,栈就放在调度器里面,不放在协程里面
3.调度器用一个while一直在运行,三个集合:①ready ,②wait,③io_wait部分
4.具体
①判断sleep是否到期
②就绪队列
③epoll里面这部分
补充:
①yield底层调用swicth交换两个协程
②resume是恢复协程的运行,yield是协程让出cpu执行权限
9.协程实现的工作流程
- 1.创建协程
当我们需要异步调用的时候,我们会创建一个协程。比如 accept 返回一个新的sockfd,创建一个客户端处理的子过程。再比如需要监听多个端口的时候,创建一个 server的子过程,这样多个端口同时工作的,是符合微服务的架构的。
创建协程的时候,进行了如何的工作?创建 API 如下:
// coroutine -->
// create
//
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg) {
assert(pthread_once(&sched_key_once, nty_coroutine_sched_key_creator) == 0);
nty_schedule *sched = nty_coroutine_get_sched();
if (sched == NULL) {
nty_schedule_create(0);
sched = nty_coroutine_get_sched();
if (sched == NULL) {
printf("Failed to create scheduler\n");
return -1;
}
}
nty_coroutine *co = calloc(1, sizeof(nty_coroutine));
if (co == NULL) {
printf("Failed to allocate memory for new coroutine\n");
return -2;
}
int ret = posix_memalign(&co->stack, getpagesize(), sched->stack_size);
if (ret) {
printf("Failed to allocate stack for new coroutine\n");
free(co);
return -3;
}
co->sched = sched;
co->stack_size = sched->stack_size;
co->status = BIT(NTY_COROUTINE_STATUS_NEW); //
co->id = sched->spawned_coroutines ++;
co->func = func;
#if CANCEL_FD_WAIT_UINT64
co->fd = -1;
co->events = 0;
#else
co->fd_wait = -1;
#endif
co->arg = arg;
co->birth = nty_coroutine_usec_now();
*new_co = co;
TAILQ_INSERT_TAIL(&co->sched->ready, co, ready_next);
return 0;
}
参数 1:nty_coroutine **new_co,需要传入空的协程的对象,这个对象是由内部
创建的,并且在函数返回的时候,会返回一个内部创建的协程对象。
参数 2:proc_coroutine func,协程的子过程。当协程被调度的时候,就会执行该
函数。
参数 3:void *arg,需要传入到新协程中的参数
- 2.io异步操作
在协程的上下文 IO 异步操作(nty_recv,nty_send)函数,步骤如下:
- 将 sockfd 添加到 epoll 管理中。
- 进行上下文环境切换,由协程上下文 yield 到调度器的上下文。
- 调度器获取下一个协程上下文。Resume 新的协程IO 异步操作的上下文切换的时序图如下:
//recv
// add epoll first
//
ssize_t nty_recv(int fd, void *buf, size_t len, int flags) {
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN | POLLERR | POLLHUP;
nty_poll_inner(&fds, 1, 1); //nty_poll_inner判断io是否准备就绪
int ret = recv(fd, buf, len, flags);
if (ret < 0) {
//if (errno == EAGAIN) return ret;
if (errno == ECONNRESET) return -1;
//printf("recv error : %d, ret : %d\n", errno, ret);
}
return ret;
}
ssize_t nty_send(int fd, const void *buf, size_t len, int flags) {
int sent = 0;
int ret = send(fd, ((char*)buf)+sent, len-sent, flags);
if (ret == 0) return ret;
if (ret > 0) sent += ret;
while (sent < len) {
struct pollfd fds;
fds.fd = fd;
fds.events = POLLOUT | POLLERR | POLLHUP;
nty_poll_inner(&fds, 1, 1); //nty_poll_inner判断io是否准备就绪
ret = send(fd, ((char*)buf)+sent, len-sent, flags);
//printf("send --> len : %d\n", ret);
if (ret <= 0) {
break;
}
sent += ret;
}
if (ret <= 0 && sent == 0) return ret;
return sent;
}
/*
* nty_poll_inner --> 1. sockfd--> epoll, 2 yield, 3. epoll x sockfd
* fds :
*/
static int nty_poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) {
//1.如果超时时间为0,直接调用poll
if (timeout == 0)
{
return poll(fds, nfds, timeout);
}
if (timeout < 0)
{
timeout = INT_MAX;
}
nty_schedule *sched = nty_coroutine_get_sched();
nty_coroutine *co = sched->curr_thread;
int i = 0;
for (i = 0;i < nfds;i ++) {
struct epoll_event ev;
ev.events = nty_pollevent_2epoll(fds[i].events);
ev.data.fd = fds[i].fd;
//2.如果io没准备就绪,就把io加入epoll里面
//(这个epoll是schedule的epoll,这个epoll是全局的,所有协程都公用一个,他是管理所有io的)
epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev);
co->events = fds[i].events;
nty_schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout);
}
//3.没就绪的io加入epoll之后,就是yield
nty_coroutine_yield(co); //1
for (i = 0;i < nfds;i ++) {
struct epoll_event ev;
ev.events = nty_pollevent_2epoll(fds[i].events);
ev.data.fd = fds[i].fd;
epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev);
nty_schedule_desched_wait(fds[i].fd);
}
return nfds;
}
- 3.协程子过程回调
在 create 协程后,何时回调子过程?何种方式回调子过程?
首先来回顾一下 x86_64 寄存器的相关知识。汇编与寄存器相关知识还会在《协程的实现之切换》继续深入探讨的。x86_64 的寄存器有 16 个 64 位寄存器,分别是:%rax, %rbx,%rcx, %esi, %edi, %rbp, %rsp, %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15。%rax 作为函数返回值使用的。 %rsp 栈指针寄存器,指向栈顶%rdi, %rsi, %rdx, %rcx, %r8, %r9 用作函数参数,依次对应第 1 参数,第 2 参数。。。%rbx, %rbp, %r12, %r13, %r14, %r15 用作数据存储,遵循调用者使用规则,换句话说,就是随便用。调用子函数之前要备份它,以防它被修改%r10, %r11 用作数据存储,就是使用前要先保存原值
以 NtyCo 的实现为例,来分析这个过程。CPU 有一个非常重要的寄存器叫做 EIP,用来存储 CPU 运行下一条指令的地址。我们可以把回调函数的地址存储到 EIP 中,将相应的参数存储到相应的参数寄存器中。实现子过程调用的逻辑代码如下:
void _exec(nty_coroutine *co) {
co->func(co->arg); //子过程的回调函数
}
void nty_coroutine_init(nty_coroutine *co) {
//ctx 就是协程的上下文
co->ctx.edi = (void*)co; //设置参数
co->ctx.eip = (void*)_exec; //设置回调函数入口
//当实现上下文切换的时候,就会执行入口函数_exec , _exec 调用子过程 func
}
//完整代码
static void _exec(void *lt) {
#if defined(__lvm__) && defined(__x86_64__)
__asm__("movq 16(%%rbp), %[lt]" : [lt] "=r" (lt));
#endif
nty_coroutine *co = (nty_coroutine*)lt;
co->func(co->arg); //
co->status |= (BIT(NTY_COROUTINE_STATUS_EXITED) | BIT(NTY_COROUTINE_STATUS_FDEOF)
| BIT(NTY_COROUTINE_STATUS_DETACH));
#if 1
nty_coroutine_yield(co);
#else
co->ops = 0;
_switch(&co->sched->ctx, &co->ctx);
#endif
}
static void nty_coroutine_init(nty_coroutine *co) {
void **stack = (void **)(co->stack + co->stack_size);
stack[-3] = NULL;
stack[-2] = (void *)co;
co->ctx.esp = (void*)stack - (4 * sizeof(void*));
co->ctx.ebp = (void*)stack - (3 * sizeof(void*));
co->ctx.eip = (void*)_exec;
co->status = BIT(NTY_COROUTINE_STATUS_READY);
}
上面的协程初始化是在resume里面做的事
int nty_coroutine_resume(nty_coroutine *co) {
if (co->status & BIT(NTY_COROUTINE_STATUS_NEW)) {
nty_coroutine_init(co);
}
nty_schedule *sched = nty_coroutine_get_sched();
sched->curr_thread = co;
_switch(&co->ctx, &co->sched->ctx);
sched->curr_thread = NULL;
nty_coroutine_madvise(co);
#if 1
if (co->status & BIT(NTY_COROUTINE_STATUS_EXITED)) {
if (co->status & BIT(NTY_COROUTINE_STATUS_DETACH)) {
printf("nty_coroutine_resume --> \n");
nty_coroutine_free(co);
}
return -1;
}
#endif
return 0;
}
调度器代码
//调度器代码
void nty_schedule_run(void) {
nty_schedule *sched = nty_coroutine_get_sched();
if (sched == NULL) return ;
while (!nty_schedule_isdone(sched)) {
// 1. expired --> sleep rbtree(sleep的情况是非常少的)
//key就是时间,value就是协程(若时间相同插入失败,那就把时间增加一点再次插)nty_schedule_sched_sleepdown
//遍历睡眠集合,将满足条件的加入到ready
//找到哪些时间到期了,resume恢复他的运行
nty_coroutine *expired = NULL; //
while ((expired = nty_schedule_expired(sched)) != NULL) {
nty_coroutine_resume(expired);
}
// 2. ready queue就绪队列(为了解决刚开始创建的状态)
//遍历等待集合,将满足添加的加入到ready
nty_coroutine *last_co_ready = TAILQ_LAST(&sched->ready, _nty_coroutine_queue);
while (!TAILQ_EMPTY(&sched->ready)) {
nty_coroutine *co = TAILQ_FIRST(&sched->ready);
TAILQ_REMOVE(&co->sched->ready, co, ready_next);
if (co->status & BIT(NTY_COROUTINE_STATUS_FDEOF)) {
nty_coroutine_free(co);
break;
}
//拿出第一个节点开始运行
nty_coroutine_resume(co);
if (co == last_co_ready) break;
}
// 3. wait rbtree(io等待,非常常见)
//使用resume恢复ready的协程运行权
nty_schedule_epoll(sched);
while (sched->num_new_events) {
int idx = --sched->num_new_events;
struct epoll_event *ev = sched->eventlist+idx;
int fd = ev->data.fd;
int is_eof = ev->events & EPOLLHUP;
if (is_eof) errno = ECONNRESET;
nty_coroutine *co = nty_schedule_search_wait(fd);
if (co != NULL) {
if (is_eof) {
co->status |= BIT(NTY_COROUTINE_STATUS_FDEOF);
}
nty_coroutine_resume(co);
}
is_eof = 0;
}
}
nty_schedule_free(sched);
return ;
}
10.如何实现yield和resume(用户态切换,比线程切换开销小很多)
1.longjmp和setjmp,longjmp跳过去,setjmp在跳回来
2.linux接口ucontex
3.汇编代码实现跳转
11.异步的四个步骤
1.客户端先初始化上下文,也就是初始化epoll
2,与初始化对应的就是结束io
3.对应的回调处理函数callback接受mysql返回的数据
4.commit一次一次提交具体的事务
12.协程一直不让出(协程是为了解决io等待挂起的问题)
- 注意:
若协程本身没有io操作的话,那么协程的意义不大;那么用线程处理意义是一样的
13.协程调度器怎么跑起来(重点)
main主函数
开始创建协程并进入协程的执行函数server
int main(int argc, char *argv[]) {
//1.创建协程
nty_coroutine *co = NULL;
//2.监听100个端口
int i = 0;
unsigned short base_port = 8888;//从8888端口开始,监听100个端口
for (i = 0;i < 100;i ++) {
unsigned short *port = calloc(1, sizeof(unsigned short));
*port = base_port + i;
//port是协程的参数
//server是协程的运行函数
nty_coroutine_create(&co, server, port); no run
}
//3.开始调度器调度
nty_schedule_run(); //run
return 0;
}
协程执行函数,流程在代码中
void server(void *arg) {
//1.创建套接字开始监听
unsigned short port = *(unsigned short *)arg;
free(arg);
int fd = nty_socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return ;
struct sockaddr_in local, remote;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
bind(fd, (struct sockaddr*)&local, sizeof(struct sockaddr_in));
listen(fd, 20);
printf("listen port : %d\n", port);
//2.获取当前时间
struct timeval tv_begin;
gettimeofday(&tv_begin, NULL);
//3.利用监听套接字获得客户端的套接字
while (1) {
socklen_t len = sizeof(struct sockaddr_in);
int cli_fd = nty_accept(fd, (struct sockaddr*)&remote, &len);
if (cli_fd % 1000 == 999) {
struct timeval tv_cur;
memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
gettimeofday(&tv_begin, NULL);
int time_used = TIME_SUB_MS(tv_begin, tv_cur);
printf("client fd : %d, time_used: %d\n", cli_fd, time_used);
}
//printf("new client comming\n");
nty_coroutine *read_co;
nty_coroutine_create(&read_co, server_reader, &cli_fd);
}
}
nty_accept的nty_poll_inner判断协程io是否就绪
//nty_accept
//return failed == -1, success > 0
int nty_accept(int fd, struct sockaddr *addr, socklen_t *len) {
int sockfd = -1;
int timeout = 1;
nty_coroutine *co = nty_coroutine_get_sched()->curr_thread;
while (1) {
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN | POLLERR | POLLHUP;
nty_poll_inner(&fds, 1, timeout); //nty_poll_inner判断io是否准备就绪
sockfd = accept(fd, addr, len);
if (sockfd < 0) {
if (errno == EAGAIN) {
continue;
} else if (errno == ECONNABORTED) {
printf("accept : ECONNABORTED\n");
} else if (errno == EMFILE || errno == ENFILE) {
printf("accept : EMFILE || ENFILE\n");
}
return -1;
} else {
break;
}
}
int ret = fcntl(sockfd, F_SETFL, O_NONBLOCK);
if (ret == -1) {
close(sockfd);
return -1;
}
int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
return sockfd;
}
nty_poll_inner
若超时为0直接调用poll,否则加入epoll中,然后yield挂起
/*
* nty_poll_inner --> 1. sockfd--> epoll, 2 yield, 3. epoll x sockfd
* fds :
*/
static int nty_poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) {
//1.如果超时时间为0,直接调用poll
if (timeout == 0)
{
return poll(fds, nfds, timeout);
}
if (timeout < 0)
{
timeout = INT_MAX;
}
nty_schedule *sched = nty_coroutine_get_sched();
nty_coroutine *co = sched->curr_thread;
int i = 0;
for (i = 0;i < nfds;i ++) {
struct epoll_event ev;
ev.events = nty_pollevent_2epoll(fds[i].events);
ev.data.fd = fds[i].fd;
//2.如果io没准备就绪,就把io加入epoll里面
//(这个epoll是schedule的epoll,这个epoll是全局的,所有协程都公用一个,他是管理所有io的)
epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev);
co->events = fds[i].events;
nty_schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout);
}
//3.没就绪的io加入epoll之后,就是yield
nty_coroutine_yield(co); //1
for (i = 0;i < nfds;i ++) {
struct epoll_event ev;
ev.events = nty_pollevent_2epoll(fds[i].events);
ev.data.fd = fds[i].fd;
epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev);
nty_schedule_desched_wait(fds[i].fd);
}
return nfds;
}
main函数的调度器运行起来
void nty_schedule_run(void) {
nty_schedule *sched = nty_coroutine_get_sched();
if (sched == NULL) return ;
while (!nty_schedule_isdone(sched)) {
// 1. expired --> sleep rbtree(sleep的情况是非常少的)
//key就是时间,value就是协程(若时间相同插入失败,那就把时间增加一点再次插)nty_schedule_sched_sleepdown
//遍历睡眠集合,将满足条件的加入到ready
//找到哪些时间到期了,resume恢复他的运行
nty_coroutine *expired = NULL; //
while ((expired = nty_schedule_expired(sched)) != NULL) {
nty_coroutine_resume(expired);
}
// 2. ready queue就绪队列(为了解决刚开始创建的状态)
//遍历等待集合,将满足添加的加入到ready
nty_coroutine *last_co_ready = TAILQ_LAST(&sched->ready, _nty_coroutine_queue);
while (!TAILQ_EMPTY(&sched->ready)) {
nty_coroutine *co = TAILQ_FIRST(&sched->ready);
TAILQ_REMOVE(&co->sched->ready, co, ready_next);
if (co->status & BIT(NTY_COROUTINE_STATUS_FDEOF)) {
nty_coroutine_free(co);
break;
}
//拿出第一个节点开始运行
nty_coroutine_resume(co);
if (co == last_co_ready) break;
}
// 3. wait rbtree(io等待,非常常见)
//使用resume恢复ready的协程运行权
nty_schedule_epoll(sched);
while (sched->num_new_events) {
int idx = --sched->num_new_events;
struct epoll_event *ev = sched->eventlist+idx;
int fd = ev->data.fd;
int is_eof = ev->events & EPOLLHUP;
if (is_eof) errno = ECONNRESET;
nty_coroutine *co = nty_schedule_search_wait(fd);
if (co != NULL) {
if (is_eof) {
co->status |= BIT(NTY_COROUTINE_STATUS_FDEOF);
}
nty_coroutine_resume(co);
}
is_eof = 0;
}
}
nty_schedule_free(sched);
return ;
}
14.多核的问题(进程都是单线程的)
1.多进程,代码改动的地方不多,几乎不需要改动,比如说ntycp是多进程的
2.多线程,比多进程复杂很多(可以每个线程绑定CPU),需要对调度器进行加锁(怎么加?举例:在nty_schedule_expired这个函数里面对树进行加锁,在红黑树里面找出一个节点需要加锁。锁定义在调度器里面)
3.x86多核指令可以实现
15.协程实现的需要提供给外接的接口
1)协程的创建
2)启动调度器schedule_loop()
3)sleep
4)read封装成nty_read类似的
5)socket()和listen()\close()、fcntl()\setsocketopt()\getsocketopt()没必要封装成异步的
6)accept()\connect()\send()\write()\recv()\read()
sendto()\recvfrom()
会引起阻塞状态的接口,需要封装成异步的,都是下面代码的流程
nty_read() //这就成了异步的read
{
fd->epoll; //1.可以先把fd都放到epoll里面
yield; //2.然后再yield让出,先让其他协程read,等待他yield让出再读自己的read
read(); //3.在调用系统的read函数
}
16.非阻塞与协程
1.若接口改为非阻塞,性能和协程一样
2.但是协程更加具有可读性
17.协程的实现方案
- 1.一种是setjmp/longjmp
- 2.另一种是 ucontext 组件,相对比第一种,它们内部(当然是用汇编语言)实现了协程的上下文切换,相较之下前者在应用上会产生相当的不确定性(比如不好封装,具体说明参考联机文档),所以后者应用更广泛一些,网上绝大多数 C 协程库也是基于 ucontext 组件实现的。
- 3.汇编代码实现