目录
🌦️3.3.3.1 co_yield_ct() && co_yield()
☀️0.前言
协程是个很重要的概念,对于向我一样的在校生而言,或者对进程和线程比较熟悉,对协程一般不太理解,协程也确实不好理解,本篇文章希望以通俗易懂的方式讲述协程,并且以著名的libco为例,看看核心的协程实现代码。
🌤️1.协程概述
协程的概念非常不好理解,虽然网上文章众多,但大多数都给人一种云里雾里的感觉,对协程的理解也有一些不同的观点,我个人认为协程就是一种可以“暂停和恢复执行”的特殊函数,我们知道一般的函数执行类似于原子操作,一旦调用之后就一直执行直到 return,而协程则允许函数执行到一半停下来,过一段时间再接着执行,乍一看似乎没有什么意义。
大家可以想象这样一种场景,大家在做暑假家庭作业的时候,美术作业需要用到画笔,而你目前家里还没有画笔,你需要在淘宝上购买并等待两天才能到,这个时候你有两种选择,一种是在这等待期间啥也不干,另一种是在等待期间做其他作业,很明显第二种效率更高,可以让你更快的完成作业。
同样的道理,假设我们在编写程序时,某个协程需要等待外部资源(例如网络请求的响应、磁盘 I/O 等),而这些操作可能需要很长时间。普通函数或线程在这种情况下会阻塞,无法继续执行其他任务,导致资源浪费。而协程则允许程序在等待期间做其他事情,等到资源准备好后再继续恢复执行,从而大大提高了程序的并发性和效率。
举个例子:假设我们编写一个程序去下载多个文件。如果使用普通函数或线程,程序会一次下载一个文件,等到这个文件下载完成后,才会开始下载下一个文件。这意味着我们必须等待网络 I/O 结束才能继续。而协程允许我们在等待某个文件下载完成的同时,可以去启动其他文件的下载任务。当第一个文件下载完成时,我们再回来处理它,从而实现了高效的并发执行。
因此,协程的意义在于,它使得程序能够处理耗时操作(如 I/O 或网络请求)时不被阻塞,从而让程序能够利用等待的时间去做其他有用的事情。这种模型在需要处理大量 I/O 密集型任务时(如网络爬虫、并发服务器等)非常有效,因为它最大化了资源的利用率。
在linux下,进程和线程可以认为是一样的,可以不做严格的区分,所以在有了线程之后为什么还需要协程呢?难道线程完成不了协程的事情吗?这个还真完成不了,核心在于线程是由操作系统调度的,用户无法控制线程的具体执行顺序或时间,而协程的调度则由程序员自己控制,可以根据任务的重要性或优先级自由切换。另一方面,如果用线程来完成协程的事情,未免太“小题大做”了,线程创建消耗资源大,在linux下,默认占用8MB,而如果只是实现函数切换,根本不需要这么大的资源消耗,所以选择了更为轻量的协程,一般只占用几KB,用这几KB就可以完成任务,所以资源利用率高,可以实现更高效的上下文切换,也减少了内核态与用户态切换的开销(线程调度),所以也可以认为协程是“用户态的可控的轻量级线程”。
🌤️2.协程设计要点
下面来探讨一下协程设计的要点,不管是哪个协程框架,都可以从下面这些角度来剖析。
⛅2.1 协程上下文切换
协程上下文切换的意思是保存当前协程的上下文,切换到待运行协程的上下文,具体来说,栈保存协程执行过程中产生的局部变量,函数调用信息以及返回地址等,寄存器保存协程当前的执行状态,如程序计数器(PC)、栈指针(SP)以及其他一些处理器寄存器的状态。
上下文切换有多种形式,如使用操作系统提供的API:ucontent;使用setjump/longjump;用汇编语言实现(libco采用,性能较好但兼容性差);采用boost库的boost.coroutine或者boost.context
⛅2.2 协程栈
通常会创建数量庞大的协程来支持高并发,协程栈内存占用就需要考虑;栈内存不可太大(浪费空间),也不可太小(容易栈溢出),这其实是很难权衡的,一旦定死,就只能针对特定的场景,无法做到通用。
libco采用的是固定栈 + 共享栈的方式,固定栈的大小默认为128KB,共享栈的大小最大为8MB;除此之外,还可以采取分段栈、拷贝栈、虚拟内存栈等。
⛅2.3 协程调度
libco采取的调度策略是栈式调度,协程队列是一个栈式的结构,每次创建的协程都置于栈顶,会立即暂停当前协程并切换至子协程中运行,子协程运行结束(或其他原因导致切换出来)后,继续切换回来执行父协程。
🌤️3. 解析libco
⛅3.1 libco概述
libco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上。libco通过仅有的几个函数接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者异步的写法,如线程库一样轻松。同时库里面提供了socket族函数的hook,使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造。
⛅3.2 libco核心结构体剖析
要剖析libco,首先从核心结构体看起,先了解这些结构体的关系,再看函数,函数其实就是对这些结构体数据进行读写。
🌥️3.2.1 libco核心结构体关系示意图
🌥️3.2.2 libco核心结构体及其参数详解
1、stCoRoutine_t:这是Libco协程的核心结构体,表示一个协程。该结构体包含协程的执行环境、运行状态、上下文、栈等信息。
※ env: 协程运行环境,指向stCoRoutineEnv_t;
※ pfn: 协程执行函数;
※ arg: 函数参数;
※ ctx: 协程的上下文信息,指向coctx_t;
※ cStart:协程是否开始;
※ cEnd:协程是否结束;
※ cIsMain:当前是否为main函数;
※ cEnableSysHook:是否运行系统hook;
※ cIsShareStack:是否使用共享栈;
※ stack_mem: 协程运行时的栈空间,指向stStackMem_t;
※ stack_sp、save_size、save_buffer:保存协程栈指针和大小,便于恢复;
※ aSpec: 协程的本地存储,类似于线程的thread-local存储。
2、stCoRoutineEnv_t:这是协程的执行环境,类似于线程的调度器。它管理所有协程的调用栈,并维护协程之间的切换关系。
※ pCallStack[128]: 协程调用栈,最后一位是当前运行的协程,前一位是当前协程的父协程(即,resume该协程的协程),只支持不超过128个协程嵌套调用;
※ iCallStackSize: 当前调用栈长度;
※ pEpoll: 协程调度器,主要是epoll,指向stCoEpoll_t;
※ pending_co: 共享栈模式下,表示被挂起(待运行)的协程;
※ occupy_co:共享栈模式下,表示正在执行的协程。
stCoRoutineEnv_t维护了当前线程中所有协程的执行状态和切换信息,每个线程都有一个协程环境。stCoRoutine_t协程会通过这个结构体进行切换。
3、stCoEpoll_t:这是协程的事件管理器,负责协程的IO事件调度。它基于epoll实现,是协程的底层调度机制。
※ iEpollFd: epoll的fd;
※ pTimeout:超时管理器;
※ pstTimeoutList:目前已超时的事件,作为中转使用,最后会合并到active;
※ pstActiveList:正在处理的活跃事件;
※ result: epoll_wait()返回的事件。
stCoEpoll_t用于管理协程的IO事件。当协程进行IO操作(如网络请求)时,stCoEpoll_t会将协程挂起,等待事件完成后再恢复该协程。
4、coctx_t:这是用于保存协程上下文的结构体,类似于线程的上下文。它保存协程的寄存器状态以及栈的信息。
※ regs: 保存寄存器状态,386架构是8个,其余架构的为14个;
※ ss_size:协程栈大小;
※ ss_sp: 协程栈指针。
每个stCoRoutine_t都有一个coctx_t结构体,用于保存当前协程的上下文。当协程被挂起或恢复时,这个上下文会被切换。
5、stStackMem_t:表示协程的栈信息,每个协程在运行时需要一块栈内存。
※ occupy_co: 使用该栈的协程;
※ stack_size: 栈大小;
※ stack_bp:stack_buffer + stack_size;
※ stack_buffer:栈底。
一个stCoRoutine_t结构体中的stack_mem指向一个stStackMem_t,表示该协程使用的栈。每个协程的执行依赖于栈内存,用于存储局部变量、函数调用信息等。
6、stShareStack_t:是共享栈模式的支持结构体。当协程切换时,不是每个协程都有独立的栈,而是多个协程可以共享一块大的栈空间。这在需要大量协程时可以节省内存。
※ alloc_idx: 当前分配的栈的索引;
※ stack_size: 每块栈的大小;
※ count: 栈的数量;
※ stack_array: 指向stStackMem_t数组,表示多块共享的栈。
当使用共享栈模式时,多个stCoRoutine_t可以共享一个stShareStack_t结构体,而不是为每个协程都分配独立的stStackMem_t。stShareStack_t负责管理这些共享栈的分配和使用。
7、stCoRoutineAttr_t:是创建协程时的属性结构体,用于配置协程的栈大小以及是否使用共享栈。
※ stack_size: 协程的栈大小(非共享栈模式下需要指定);
※ share_stack: 是否使用共享栈。
在创建协程时,可以通过stCoRoutineAttr_t来指定协程的栈大小或选择共享栈模式。
⛅3.3 libco核心函数解析
🌥️3.3.1 初始化函数
有了这些关键结构体之后,首先要对其进行初始化,来看看创建用的初始化函数是如何操作的
🌦️3.3.1.1 co_create()
int co_create(stCoRoutine_t **ppco, const stCoRoutineAttr_t *attr, pfn_co_routine_t pfn, void *arg)
{
if (!co_get_curr_thread_env()) // 检查协程环境是否已初始化
{
co_init_curr_thread_env();
}
stCoRoutine_t *co = co_create_env(co_get_curr_thread_env(), attr, pfn, arg);
*ppco = co;
return 0;
}
可以看到co_create函数主要是调用co_create_env()来进行初始化
🌦️3.3.1.2 co_create_env()
struct stCoRoutine_t *co_create_env(stCoRoutineEnv_t *env, const stCoRoutineAttr_t *attr,
pfn_co_routine_t pfn, void *arg)
{
stCoRoutineAttr_t at;
if (attr)
{
memcpy(&at, attr, sizeof(at));
}
if (at.stack_size <= 0)
{
at.stack_size = 128 * 1024; // 默认128KB
}
else if (at.stack_size > 1024 * 1024 * 8)
{
at.stack_size = 1024 * 1024 * 8; // 最大8MB
}
if (at.stack_size & 0xFFF) // 对齐处理,确保大小为4KB的倍数
{
at.stack_size &= ~0xFFF;
at.stack_size += 0x1000;
}
stCoRoutine_t *lp = (stCoRoutine_t *)malloc(sizeof(stCoRoutine_t));
memset(lp, 0, (long)(sizeof(stCoRoutine_t)));
lp->env = env;
lp->pfn = pfn;
lp->arg = arg;
stStackMem_t *stack_mem = NULL;
if (at.share_stack)
{
// 如果采用了共享栈模式,则获取到其中一个共享栈的内存
stack_mem = co_get_stackmem(at.share_stack);
at.stack_size = at.share_stack->stack_size;
}
else
{
// 如果没有采用共享栈,则分配内存
stack_mem = co_alloc_stackmem(at.stack_size);
}
lp->stack_mem = stack_mem;
// 设置协程上下文
lp->ctx.ss_sp = stack_mem->stack_buffer; // 栈地址
lp->ctx.ss_size = at.stack_size; // 栈大小
// 初始化协程状态
lp->cStart = 0; // 默认未启动
lp->cEnd = 0; // 默认未结束
lp->cIsMain = 0; // 默认非主协程
lp->cEnableSysHook = 0; // 默认不开启hook
lp->cIsShareStack = at.share_stack != NULL;
// 如果使用共享栈,则初始化save_size和save_buffer
lp->save_size = 0;
lp->save_buffer = NULL;
return lp;
}
创建函数主要是对协程结构体参数进行赋初值,默认是不开启hook的,默认栈大小为128KB,最大栈大小为8MB。总体函数是比较简单的,对照着注释可以很容易看懂
🌥️3.3.2 恢复(resume)函数
初始化结束后我们来看看如何启用某个协程
🌦️3.3.2.1 co_resume()
void co_resume(stCoRoutine_t *co)
{
stCoRoutineEnv_t *env = co->env; // 获取当前协程的运行环境
stCoRoutine_t *lpCurrRoutine = env->pCallStack[env->iCallStackSize - 1]; // 获取当前正在执行的协程
if (!co->cStart) // 如果目标协程还没有启动
{
coctx_make(&co->ctx, (coctx_pfn_t)CoRoutineFunc, co, 0); // 为目标协程设置上下文
co->cStart = 1; // 标记协程已启动
}
env->pCallStack[env->iCallStackSize++] = co; // 将目标协程推入调用栈,准备执行
co_swap(lpCurrRoutine, co); // 切换到目标协程的上下文
}
co_resume函数内部调用co_swap()来完成上下文切换
🌦️3.3.2.2 co_swap()
/*
* 1. 将当前的运行上下文保存到curr中
* 2. 将当前的运行上下文替换为pending_co中的上下文
*/
void co_swap(stCoRoutine_t *curr, stCoRoutine_t *pending_co)
{
stCoRoutineEnv_t *env = co_get_curr_thread_env(); // 获取当前线程的协程环境
char c;
curr->stack_sp = &c; // 保存当前协程的栈顶位置
if (!pending_co->cIsShareStack) // 如果目标协程没有使用共享栈
{
env->pending_co = NULL;
env->occupy_co = NULL;
}
else // 如果目标协程使用了共享栈
{
env->pending_co = pending_co; // 将目标协程标记为待执行协程
stCoRoutine_t *occupy_co = pending_co->stack_mem->occupy_co; // 获取当前占用共享栈的协程
pending_co->stack_mem->occupy_co = pending_co; // 将目标协程设为占用共享栈的协程
env->occupy_co = occupy_co;
if (occupy_co && occupy_co != pending_co) // 如果共享栈已经被别的协程占用
{
save_stack_buffer(occupy_co); // 将占用共享栈的协程的栈内容保存到其他位置
}
}
coctx_swap(&(curr->ctx), &(pending_co->ctx)); // 切换上下文
// 恢复共享栈的内容
stCoRoutineEnv_t *curr_env = co_get_curr_thread_env();
stCoRoutine_t *update_occupy_co = curr_env->occupy_co;
stCoRoutine_t *update_pending_co = curr_env->pending_co;
if (update_occupy_co && update_pending_co
&& update_occupy_co != update_pending_co) // 共享栈模式下恢复栈数据
{
if (update_pending_co->save_buffer && update_pending_co->save_size > 0)
{
memcpy(update_pending_co->stack_sp, update_pending_co->save_buffer,
update_pending_co->save_size); // 将保存的栈数据恢复
}
}
}
co_swap()还是比较复杂的,需要好好理解,其实保存当前协程的栈顶位置需要解释一下:
char c;
curr->stack_sp = &c; // 保存当前协程的栈顶位置
当你调用一个函数时,栈顶会随着函数的局部变量和函数调用信息的增加而移动。局部变量 c 是在 co_swap 函数中定义的,它会被分配在当前栈帧的栈顶位置。通过取 c 的地址 &c,可以得到当前栈顶的位置,因为此时 c 是栈上最靠近栈顶的变量。curr->stack_sp = &c 这行代码的作用是将当前协程的栈指针保存起来,这样当协程被挂起时(即上下文切换后),可以记录它在栈上的位置,以便后续恢复。
co_swap()调用coctx_swap()来完成真正的上下文切换
🌦️3.3.2.3 coctx_swap()
用汇编语言编写的协程上下文切换函数,负责保存当前协程的 CPU 寄存器状态,并加载目标协程的寄存器状态,从而实现协程的切换,分32位系统和64位系统。
.globl coctx_swap
#if !defined( __APPLE__ )
.type coctx_swap, @function
#endif
coctx_swap:
#if defined(__i386__)
movl 4(%esp), %eax # 从栈中取出第一个参数,存储在 eax 中
movl %esp, 28(%eax) # 将当前的栈指针 (%esp) 保存到当前协程上下文中的 esp
movl %ebp, 24(%eax) # 将当前基指针 (%ebp) 保存到当前协程上下文中的 ebp
movl %esi, 20(%eax) # 保存其他寄存器 esi
movl %edi, 16(%eax) # 保存其他寄存器 edi
movl %edx, 12(%eax) # 保存其他寄存器 edx
movl %ecx, 8(%eax) # 保存其他寄存器 ecx
movl %ebx, 4(%eax) # 保存其他寄存器 ebx
movl 8(%esp), %eax # 从栈中取出第二个参数,存储在 eax 中
movl 4(%eax), %ebx # 恢复目标协程上下文的 ebx
movl 8(%eax), %ecx # 恢复目标协程上下文的 ecx
movl 12(%eax), %edx # 恢复目标协程上下文的 edx
movl 16(%eax), %edi # 恢复目标协程上下文的 edi
movl 20(%eax), %esi # 恢复目标协程上下文的 esi
movl 24(%eax), %ebp # 恢复目标协程上下文的 ebp
movl 28(%eax), %esp # 恢复目标协程上下文的 esp
ret # 返回到目标协程执行
#elif defined(__x86_64__)
leaq (%rsp), %rax # 将当前的栈指针地址存储到 rax 中
movq %rax, 104(%rdi) # 保存当前的栈指针 (%rsp) 到当前协程上下文中的 rsp
movq %rbx, 96(%rdi) # 保存当前协程的 rbx 寄存器值
movq %rcx, 88(%rdi) # 保存 rcx 寄存器
movq %rdx, 80(%rdi) # 保存 rdx 寄存器
movq %rsi, 64(%rdi) # 保存 rsi 寄存器
movq %rdi, 56(%rdi) # 保存 rdi 寄存器
movq %rbp, 48(%rdi) # 保存基指针寄存器 rbp
movq %r8, 40(%rdi) # 保存 r8 寄存器
movq %r9, 32(%rdi) # 保存 r9 寄存器
movq %r12, 24(%rdi) # 保存 r12 寄存器
movq %r13, 16(%rdi) # 保存 r13 寄存器
movq %r14, 8(%rdi) # 保存 r14 寄存器
movq %r15, (%rdi) # 保存 r15 寄存器
xorq %rax, %rax # 将 rax 清零
movq 48(%rsi), %rbp # 恢复目标协程的 rbp
movq 104(%rsi), %rsp # 恢复目标协程的 rsp
movq (%rsi), %r15 # 恢复目标协程的 r15
movq 8(%rsi), %r14 # 恢复 r14
movq 16(%rsi), %r13 # 恢复 r13
movq 24(%rsi), %r12 # 恢复 r12
movq 32(%rsi), %r9 # 恢复 r9
movq 40(%rsi), %r8 # 恢复 r8
movq 56(%rsi), %rdi # 恢复 rdi
movq 80(%rsi), %rdx # 恢复 rdx
movq 88(%rsi), %rcx # 恢复 rcx
movq 96(%rsi), %rbx # 恢复 rbx
leaq 8(%rsp), %rsp # 调整栈指针
pushq 72(%rsi) # 将目标协程的返回地址压入栈
movq 64(%rsi), %rsi # 恢复 rsi
ret # 返回到目标协程
#endif
🌥️3.3.3 挂起(yield)函数
看完恢复函数,我们再来看看挂起函数,其实主函数还是co_swap()
🌦️3.3.3.1 co_yield_ct() && co_yield()
void co_yield_ct()
{
co_yield_env(co_get_curr_thread_env());
}
void co_yield (stCoRoutine_t *co)
{
co_yield_env(co->env);
}
🌦️3.3.3.2 co_yield_env()
void co_yield_env(stCoRoutineEnv_t *env)
{
stCoRoutine_t *last = env->pCallStack[env->iCallStackSize - 2]; // 上一个协程(当前协程的父协程)
stCoRoutine_t *curr = env->pCallStack[env->iCallStackSize - 1]; // 当前正在执行的协程
env->iCallStackSize--; // 减少调用栈的大小(将当前协程移出调用栈)
co_swap(curr, last); // 在当前协程和父协程之间进行上下文切换
}
至此,libco的核心函数我们都看完了,整个协程实现的核心就在这里,其实代码并不长。上述代码是在一个线程内部完成不同代码之间的切换,都是用户手动掌控切换时机的,如果是无法精准预测收发数据时机的网络IO呢?就需要复用epoll了。
或许有同学有个疑问是协程可以用在哪里,一般来说可以和epoll结合来实现TcpServer,我们下面就来看一下这个libco的使用示例。
🌤️4. libco使用实例——基于epoll实现echo
这段代码在example_echosrv.c文件。
⛅4.1 main()函数
我们从main函数看起,看看协程到底是如何运用在TcpServer上的。
int main(int argc, char *argv[])
{
if (argc < 5)
{
printf("Usage:\n"
"example_echosvr [IP] [PORT] [TASK_COUNT] [PROCESS_COUNT]\n"
"example_echosvr [IP] [PORT] [TASK_COUNT] [PROCESS_COUNT] -d # daemonize mode\n");
return -1;
}
const char *ip = argv[1]; // IP地址
int port = atoi(argv[2]); // 端口号
int cnt = atoi(argv[3]); // 协程数量
int proccnt = atoi(argv[4]); // 进程数量
bool deamonize = argc >= 6 && strcmp(argv[5], "-d") == 0;
g_listen_fd = CreateTcpSocket(port, ip, true); // 创建监听fd并绑定
listen(g_listen_fd, 1024); // 开始监听
if (g_listen_fd == -1)
{
printf("Port %d is in use\n", port);
return -1;
}
printf("listen %d %s:%d\n", g_listen_fd, ip, port);
SetNonBlock(g_listen_fd); // 设置非阻塞模式
for (int k = 0; k < proccnt; k++) // 多进程处理
{
pid_t pid = fork();
if (pid > 0) continue;
else if (pid < 0) break;
for (int i = 0; i < cnt; i++) // 在每个子进程中,创建并启动多个协程,每个协程处理一个任务
{
task_t *task = (task_t *)calloc(1, sizeof(task_t));
task->fd = -1;
co_create(&(task->co), NULL, readwrite_routine, task); // 创建一个协程
co_resume(task->co); // 启动协程
}
// 启动accept协程
stCoRoutine_t *accept_co = NULL;
co_create(&accept_co, NULL, accept_routine, 0);
co_resume(accept_co);
co_eventloop(co_get_epoll_ct(), 0, 0); // 启动事件循环
exit(0);
}
if (!deamonize) wait(NULL);
return 0;
}
相信配合着注释很好理解,在该函数中,两个核心的函数在于处理客户端连接的协程和数据读写协程,我们来分别看一下。
⛅4.2 accept_routine()
static void *accept_routine(void *) // 处理客户端连接的协程
{
co_enable_hook_sys();
printf("accept_routine\n");
fflush(stdout);
for (;;)
{
if (g_readwrite.empty()) // 如果没有空闲的协程可以处理连接请求
{
printf("empty\n");
struct pollfd pf = {0};
pf.fd = -1;
poll(&pf, 1, 1000); // 睡眠一秒等待有空闲的协程
continue;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t len = sizeof(addr);
int fd = co_accept(g_listen_fd, (struct sockaddr *)&addr, &len);
if (fd < 0) // 如果accept失败,调用co_poll()暂时让出执行权
{
struct pollfd pf = {0};
pf.fd = g_listen_fd;
pf.events = (POLLIN | POLLERR | POLLHUP);
co_poll(co_get_epoll_ct(), &pf, 1, 1000);
continue;
}
if (g_readwrite.empty())
{
close(fd);
continue;
}
SetNonBlock(fd); // 设置非阻塞
// 处理连接
task_t *co = g_readwrite.top();
co->fd = fd;
g_readwrite.pop();
co_resume(co->co);
}
return 0;
}
⛅4.3 readwrite_routine()
static void *readwrite_routine(void *arg) // 处理客户端读写操作的协程
{
co_enable_hook_sys();
task_t *co = (task_t *)arg;
char buf[1024 * 16];
for (;;)
{
if (-1 == co->fd)
{
g_readwrite.push(co); // 当前协程没有任务,将其放入空闲队列
co_yield_ct(); // 暂停当前协程,等待被唤醒
continue;
}
int fd = co->fd;
co->fd = -1;
for (;;)
{
struct pollfd pf = {0};
pf.fd = fd;
pf.events = (POLLIN | POLLERR | POLLHUP);
co_poll(co_get_epoll_ct(), &pf, 1, 1000); // 等待可读事件
int ret = read(fd, buf, sizeof(buf)); // 当超时或者可读事件到达时,进行read
if (ret > 0) ret = write(fd, buf, ret); // 将读取的数据写回客户端
if (ret <= 0)
{
if (errno == EAGAIN) continue;
close(fd);
break;
}
}
}
return 0;
}
相信只要是熟悉常规的TcpServer的同学都可以很容易看懂上述代码。
🪐5. 结束语
这次暂时就分享到这里,只是介绍了libco中核心的部分,理解了这部分也就理解了协程到底是如何实现的,libco还有一些比较重要的东西,如时间轮等,以后有时间了再把这部分内容补上。本人目前还是个在校生,还比较小白,也刚刚开始写 CSDN 博客不久,可能写的也不是很好,如果有任何疑问或者发现我有哪里写的不对的地方,欢迎大家留言告诉我!我都会一一改正的。
如果觉得文章对你有帮助的话,还请点赞,关注,收藏支持小占!