协程源码:https://github.com/wuj1nquan/bitco ,每一行都有详细注释
进阶篇:
协程如何解决单线程并发?
首先作者尊重历史,协程的提出及最初实现者是Melvin Conway
先解释什么是协程:
协程(Coroutine)是一种计算机程序组件,它允许在特定的位置暂停执行,并在稍后恢复执行。
协程组件通常由以下几个主要部分组成:
- 协程对象(Coroutine Object):表示一个协程,它可以在特定的位置暂停执行,并在稍后恢复执行。协程对象通常由编程语言或框架提供的特殊语法或函数创建。
- 调度器(Scheduler):负责管理和调度多个协程的执行。调度器决定何时暂停一个协程并切换到另一个协程,以实现并发执行。
- 事件循环(Event Loop):是调度器的一种实现,它负责监控协程的状态并决定何时执行哪个协程。事件循环通常是异步编程中的核心组件,它驱动整个协程的执行过程。
- 协程间通信机制(Inter-coroutine Communication Mechanism):用于协程之间的通信和数据交换。这可以是通过共享变量、消息传递、管道等方式实现的。
- 异常处理机制(Exception Handling Mechanism):处理协程执行过程中可能出现的异常情况。异常处理机制可以捕获和处理协程中的异常,并根据需要采取适当的措施。
这些组件共同构成了协程的基本框架,使得程序员可以使用协程来编写高效、可维护的异步代码,实现并发执行和异步操作。不同的编程语言和框架可能提供不同的实现方式和特性,但通常都会包含类似的组件来支持协程。
而一个协程对象实际上就是一个支持被调度器调度的函数
本文讨论目前需要协程的原因:
在高并发的服务器请求下,我们想尽可能提高效率,当然可以通过多线程解决,但这是人傻钱多的举动,如果创建销毁一个线程只为了处理一个简单的请求,我们觉得这样很不划算,甚至是一种性能浪费。
答案:单线程处理多任务
这可能吗?单线程一次只能处理一个任务,同时处理多个任务不是并发吗,确定没搞错成多线程和多进程吗?
首先,没搞错,我们的目的是用单线程处理多任务,但不是同时,考虑操作系统是如何实现并发的,单个cpu上多个进程也不是同时运行的,而是时间分片为我们造成了多个进程同时运行的假象。
解决方案:模拟时间分片
先来看看没有协程的单线程中多任务是如何处理的(伪代码)
↓ 时间线
↓
任务1
send(request); //客户端发送请求 1
等待服务端回复; time...
recv();//接收数据
任务2
send(request); //客户端发送请求 2
等待服务端回复; time...
recv();//接收数据
任务3
send(request); //客户端发送请求 3
等待服务端回复; time...
recv();//接收数据
重点在这里!!!
可以看到这三个请求是按顺序依次执行的,任务二需要等待任务一执行完后开始执行,然而任务一在运行时也需要等待,这样一来,不只是任务一在等待time,而是三个任务都在等待任务一的time,这时整个线程都在等待,什么都没有做!
就像你需要烧水,洗衣服,做饭。先打水、烧水然后什么也不干,等着水烧开再去洗衣服!
第二个例子我们都会解决:先烧上水,然后去开洗衣机、再去做饭,做饭时找空闲时间,每隔几分钟看看水烧开了没有,衣服洗好了没有
所以,单线程能不能也这样做呢?
答案是肯定的,我们需要在任务一等待时切换到任务二...切换回任务一... 如此一来,我们避免了大量的等待时间!
核心问题:
如何实现任务切换(调度器)?
- 利用 glibc 的 ucontext 组件(使用起来最简单)
- 使用汇编代码来切换上下文(效率最高)
- 利用 C 语言的 setjmp 和 longjmp(可移植性最好)
实现任务切换
这里用ucontext举例说明(因为我不会汇编0.0)
// ucontext_t结构体 用于保存线程的执行状态(上下文) ucontext_t ctx[3]; ucontext_t main_ctx; int count = 0; // coroutine 1 void func1(void) { while(count++ < 30) { printf("1\n"); swapcontext(&ctx[0], &main_ctx);// context switch printf("2\n"); } } // coroutine 2 void func2(void) { while(count++ < 30) { printf("3\n"); swapcontext(&ctx[1], &main_ctx);// context switch printf("4\n"); } } //coroutine 3 void func3(void) { while(count++ < 30) { printf("5\n"); swapcontext(&ctx[2], &main_ctx);// context switch printf("6\n"); } } // schedule int main() { // 每个上下文的栈空间 char stack1[STACK_SIZE] = {0}; char stack2[STACK_SIZE] = {0}; char stack3[STACK_SIZE] = {0}; // getcontext初始化结构体全部属性 getcontext(&ctx[0]);// 将当前线程的执行状态保存到ctx[0]结构体中 // 自定义设置结构体部分属性 ctx[0].uc_stack.ss_sp = stack1; ctx[0].uc_stack.ss_size = sizeof(stack1); ctx[0].uc_link = &main_ctx; makecontext(&ctx[0], func1, 0); //修改ctx[0]的上下文为func1的上下文 getcontext(&ctx[1]); ctx[1].uc_stack.ss_sp = stack2; ctx[1].uc_stack.ss_size = sizeof(stack2); ctx[1].uc_link = &main_ctx; makecontext(&ctx[1], func2, 0); getcontext(&ctx[2]); ctx[2].uc_stack.ss_sp = stack3; ctx[2].uc_stack.ss_size = sizeof(stack3); ctx[2].uc_link = &main_ctx; makecontext(&ctx[2], func3, 0); printf("swapcontext\n"); while (count <= 30) { // scheduler swapcontext(&main_ctx, &ctx[count%3]); // 这里的main_ctx未被初始化,仅用作其他上下文之间的调度 } printf("\n"); return 0; }
部分运行结果:
swapcontext 1 3 5 2 1 4 3 6 5 2 1 4 3 6 5 2 1