【Linux】协程简介

2 篇文章 0 订阅
1 篇文章 0 订阅

一、什么是协程?

首先回忆一下线程的概念:
线程是操作系统能够进行运算调度的最小单位。被包含在进程之中,是进程的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程可以并发执行多个线程,每个线程会执行不同的任务。

简介

  • 协程可以理解为一种用户态的轻量级线程,切换由用户定义,各任务之间可以控制执行暂停恢复函数,来达到多任务协作的目的;
  • 协程上下文切换速度快, 且不会陷入内核态
  • 协程拥有独立的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈,在来回切换时,恢复先前保存的寄存器上下文和栈。
  • 一个线程中可以有多个协程,协程是运行在线程之中的逻辑处理单元。协程在线程里的运行是串行的。
  • 协程之间的调度通过调度器或者自己主动放开对CPU的占有,让给其他协程。

优点

  • 协程具有极高的执行效率,由于子程序切换不是线程切换,是由程序自身控制,故协程没有线程切换的开销;
  • 多线程的线程数越多,协程的性能越显著
  • 访问共享资源不需要使用多线程的锁机制和变量冲突,由于只有一个线程,故在协程只需要判断状态即可,降低了编码难度;
  • 同步代码的方式写异步逻辑;

二、为什么使用协程?

为什么使用协程,我们先从server框架的实现说起,对于client-server的架构,server最简单的实现如下。即串行地接收连接、读取请求、处理、应答。该实现弊端显而易见,server同一时间只能为一个客户端服务。

while(1) {
	accept();
	recv();
	do();
	send();
	}

为充分利用好多核cpu进行任务处理,我们有了多进程/多线程的server框架,这也是server最常用的实现方式
accept进程 - n个epoll进程 - n个worker进程

  • accpet进程处理到来的连接,并将fd交给各个epoll进程
  • epoll进程对各fd设置监控事件,当事件触发时通过共享内存等方式,将请求传给各个worker进程
  • worker进程负责具体的业务逻辑处理并回包应答

以上框架以事件监听、进程池的方式,解决了多任务处理问题,但我们还可以对其作进一步的优化。

进程/线程是Linux内核最小的调度单位,一个进程在进行io操作时 (常见于分布式系统中RPC远程调用),其所在的cpu也处于iowait状态。直到后端svr返回,或者该进程的时间片用完、进程被切换到就绪态。是否可以把原本用于iowait的cpu时间片利用起来,发生io操作时让cpu处理新的请求,以提高单核cpu的使用率?

协程在用户态下完成切换,由程序员/调度器完成调度,结合对socket类/io操作类函数挂钩子、添加事件监听,为以上问题提供了解决方法。

三、协程的种类

协程目前分两种,一种是go语言采用的对称协程;一种是libco采用的非对称协程
yield: 协程执行到⼀半就退出,暂时让出CPU执行权
resume: 协程重新恢复运行

1、对称协程

这里借用知乎博主tx征服者的图来向大家说明:
在这里插入图片描述

对称协程其实就是由协程调度器来负责,协程不允许调度其他协程。 调度器resume协程A,那么协程A会yeild回调度器,再由调度器去执行其他的协程。如果我们把resume的虚线都放在左边,yeild的实线都放在右边。以调度器为中心,那么他是不是就是一个对称的图形呢?

2、非对称协程

我们再来看一下非对称协程
在这里插入图片描述
非对称调度由调度器来调度协程A,然后协程A再调动协程B。 那么Byeild让出CPU使用权就不是让给调度器了,而是协程A。简而言之就是从哪儿来,回哪儿去。同样,将此展开也不是一个对称的图形了。

四、协程栈

1、静态栈

固定大小的栈,容易造成溢出等现象。

2、分段栈

插入栈内存检测代码,若栈不够用,则申请新内存扩展;但该方法难以在第三方库中进行使用。

3、共享栈

申请一块大内存作为共享栈,在运行前,先把协程栈的内存copy到共享栈中,运行结束后再计算协程栈真正使用的内存,copy出来保存起来,这样每次只需保存真正使用到的栈内存量即可。

优点:该方案极大程度上避免了内存的浪费,做到了用多少占多少,同等内存条件下,可以启动的协程数量更多。

缺点:但该方案在copy上花费了时间,降低速度,导致协程切换慢。

4、虚拟内存栈

机制:进程申请的内存并不会立即被映射成物理内存,而是仅管理于虚拟内存中,真正对其读写时会触发缺页中断,此时才会映射为物理内存;
可以做到用多少占多少,冗余不超过一个内存页大小。

五、协程调度

1、栈式调度

协程队列是一个栈式结构,创建的协程都置于栈顶,并且会立即暂停当前协程并切换至子协程中运行,子协程运行结束后,继续切换回来执行父协程;越是栈底部的协程,被调度到的机会将越少,甚至出现只有栈顶的协程在互相切换。

2、星切调度

调度线程 -> 协程A -> 调度线程 -> 协程B -> 调度线程 -> …
将当前可调度的协程组织成队列,按顺序从头部取出协程调度;新协程则从尾部入队,调度后再将协程从尾部入队。

3、环切调度

调度线程 -> 协程A -> 协程B -> 协程C -> 协程D -> 调度线程 -> …
从调度顺序上可知,环切的切换次数仅为星切的一半,可以提高整体切换速度;但在多线程调度、WorkSteal方面会带来一定的挑战。

六、常见协程库

boost.context
提供了上下文的抽象,并给了两种方式,fiber和call/cc的方式保留和执行上下文切换;
性能佳,推荐使用,切换性能可达到1.25亿次/秒。

boost.coroutine
提供的协程只能单向传递数据,数据只能单向的从一个代码块流向另一个代码块。流入流出分别对应着push_type和pull_type类型,由这两个类型组成协程间跳转的通道,同时也是数据传递的通道。

ucontext
该库是在unix下提供的,使用是最安全可靠,但性能较差,大概200万次/秒。

fiber
该库是在window下提供的,与ucontext类似。

libco
腾讯开源的c++协程库。

libgo
libgo为了有更广阔的适用性,支持了多线程调度、HookSyscall、Worksteal等,同时突破了传统协程库仅用来处理网络io密集型业务的局限,也能适用于cpu密集型业务,充当并行编程库来使用。

参考文献

协程 及 libco 介绍
ucontext-人人都可以实现的简单协程库
协程学习(对称和非对称)
Linux【协程】 | 常见协程库简介

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Hyperf 是基于 Swoole 4.5+ 实现的高性能、高灵活性的 PHP 协程框架,内置协程服务器及大量常用的组件,性能较传统基于 PHP-FPM 的框架有质的提升,提供超高性能的同时,也保持着极其灵活的可扩展性,标准组件均基于 PSR 标准 实现,基于强大的依赖注入设计,保证了绝大部分组件或类都是可替换 与可复用的。 框架组件库除了常见的协程版的 MySQL 客户端、Redis 客户端,还为您准备了协程版的 Eloquent ORM、WebSocket 服务端及客户端、JSON RPC 服务端及客户端、GRPC 服务端及客户端、Zipkin/Jaeger (OpenTracing) 客户端、Guzzle HTTP 客户端、Elasticsearch 客户端、Consul 客户端、ETCD 客户端、AMQP 组件、NSQ 组件、Nats 组件、Apollo 配置心、阿里云 ACM 应用配置管理、ETCD 配置心、基于令牌桶算法的限流器、通用连接池、熔断器、Swagger 文档生成、Swoole Tracker、视图引擎、Snowflake 全局 ID 生成器 等组件,省去了自己实现对应协程版本的麻烦。 Hyperf 还提供了基于 PSR-11 的依赖注入容器、注解、AOP 面向切面编程、基于 PSR-15 的间件、自定义进程、基于 PSR-14 的事件管理器、Redis/RabbitMQ/NSQ/Nats 消息队列、自动模型缓存、基于 PSR-16 的缓存、Crontab 秒级定时任务、Translation 国际化、Validation 验证器等非常便捷的功能,满足丰富的技术场景和业务场景,开箱即用。 框架初衷: 尽管现在基于 PHP 语言开发的框架处于一个百家争鸣的时代,但仍旧未能看到一个优雅的设计与超高性能的共存的完美框架,亦没有看到一个真正为 PHP 微服务铺路的框架,此为 Hyperf 及其团队成员的初衷,我们将持续投入并为此付出努力,也欢迎你加入我们参与开源建设。 设计理念: Hyperspeed + Flexibility = Hyperf,从名字上我们就将超高速和灵活性作为 Hyperf 的基因。 对于超高速,我们基于 Swoole 协程并在框架设计上进行大量的优化以确保超高性能的输出。 对于灵活性,我们基于 Hyperf 强大的依赖注入组件,组件均基于 PSR 标准的契约和由 Hyperf 定义的契约实现,达到框架内的绝大部分的组件或类都是可替换的。 基于以上的特点,Hyperf 将存在丰富的可能性,如实现 Web 服务,网关服务,分布式间件,微服务架构,游戏服务器,物联网(IOT)等。 运行环境: Linux, OS X or Cygwin, WSL PHP 7.2+ Swoole 4.4+
Linux C++ ,可以使用协程库来实现线程分配协程。以下是一个简单的案例: ```cpp #include <iostream> #include <ucontext.h> #include <vector> #include <pthread.h> #define STACK_SIZE 1024*1024 using namespace std; vector<ucontext_t> contexts; // 存储协程上下文 int curr_context; // 当前执行的协程 void* run(void* arg) { while (1) { cout << "Thread " << pthread_self() << " running" << endl; swapcontext(&contexts[curr_context], &contexts[curr_context+1]); // 切换到下一个协程 curr_context = (curr_context + 1) % contexts.size(); } return NULL; } int main() { pthread_t threads[2]; ucontext_t main_context, new_context; // 初始化主协程上下文 getcontext(&main_context); // 创建两个协程上下文 for (int i = 0; i < 2; i++) { ucontext_t context; getcontext(&context); context.uc_link = &main_context; context.uc_stack.ss_sp = (char*)malloc(STACK_SIZE); context.uc_stack.ss_size = STACK_SIZE; makecontext(&context, [](){ cout << "Coroutine running" << endl; swapcontext(&contexts[curr_context], &contexts[curr_context+1]); // 切换到下一个协程 curr_context = (curr_context + 1) % contexts.size(); }, 0); contexts.push_back(context); } // 创建两个线程 for (int i = 0; i < 2; i++) { pthread_create(&threads[i], NULL, run, NULL); } // 等待两个线程结束 for (int i = 0; i < 2; i++) { pthread_join(threads[i], NULL); } return 0; } ``` 在该案例,我们使用了 `ucontext` 库来创建协程上下文,并使用了 `pthread` 库来创建线程。在 `main` 函数,我们首先初始化了主协程的上下文,然后创建了两个协程上下文,并将它们存储在 `contexts` 。在每个协程上下文,我们使用 `makecontext` 函数来定义协程的执行函数,并将该函数与上下文绑定。在 `run` 函数,我们不断地切换协程,直到程序结束。最后,我们创建了两个线程并等待它们结束。 需要注意的是,该案例协程切换是协作式的,即只有在协程主动让出 CPU 时,其他协程才有机会运行。如果需要实现抢占式的协程调度,需要使用更加复杂的技术。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

将月藏进诗尾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值