[转帖]协程切换和线程切换

切换,线程 · 浏览次数 : 0

小编点评

**结论:** * 协程切换比线程切换更快,因为协程切换只保存当前协程的CPU寄存器状态,而线程切换需要保存所有线程的CPU寄存器状态。 * 协程切换在用户态进行,不会陷入内核,因此协程切换的开销是相当小的。 * 协程可以看作是轻量级的线程,也是用户态的线程,用户态是指协程的切换完全是在用户态进行的,而不会陷入内核。

正文

先说结论:协程切换比线程切换快主要有两点:

(1)协程切换完全在用户空间进行,线程切换涉及特权模式切换,需要在内核空间完成;(2)协程切换相比线程切换做的事情更少

协程切换只涉及基本的CPU上下文切换,所谓的 CPU 上下文,就是一堆寄存器,里面保存了 CPU运行任务所需要的信息:从哪里开始运行(%rip:指令指针寄存器,标识 CPU 运行的下一条指令),栈顶的位置(%rsp: 是堆栈指针寄存器,通常会指向栈顶位置),当前栈帧在哪(%rbp 是栈帧指针,用于标识当前栈帧的起始位置)以及其它的CPU的中间状态或者结果(%rbx,%r12,%r13,%14,%15 等等)。协程切换非常简单,就是把当前协程的 CPU 寄存器状态保存起来,然后将需要切换进来的协程的 CPU 寄存器状态加载的 CPU 寄存器上就 ok 了。而且完全在用户态进行,一般来说一次协程上下文切换最多就是几十ns 这个量级。

一、函数栈

我们知道,在函数执行过程中,其实就是在一块栈空间内运行罢了。这个栈空间由rsp、rbp(寄存器)指针来指定两端范围。函数中的局部变量的都会存放在栈中某一块内存中。

函数调用无非是从一个函数栈跳转到相邻另一个函数栈罢了,只是由于调用返回后还需恢复原函数栈的状态,因此必须在调用时通过寄存器和栈空间的配合来存储一些数据,方便调用完成后恢复。

会通过一个例子来详细的说明函数调用的过程。

首先是一个非常简单的C文件

  1. int add(int a, int b) {
  2. return a+b;
  3. }
  4. int main() {
  5. int a = 1;
  6. int b = 2;
  7. int x = add(a, b);
  8. return 0;
  9. }

在64位的Linux系统下,通过gcc编译器将其编译为汇编文件。

  1. gcc -S add.c -o add.S
  2. // -S参数说明只编译、不汇编。因此生成的文件就是汇编代码

去除掉一些编译器附加的额外信息,保留主要的汇编代码,并手动添加注释后如下:

  1. add:
  2. pushq %rbp ; 将main函数的rbp所存值压入栈
  3. movq %rsp, %rbp ; rsp的值赋值给rbp
  4. movl %edi, -4(%rbp) ; 取第1个参数
  5. movl %esi, -8(%rbp) ; 取第二个参数
  6. movl -8(%rbp), %eax
  7. movl -4(%rbp), %edx
  8. addl %edx, %eax
  9. popq %rbp ; 栈中弹出main函数栈的rbp地址,赋值给rbp寄存器,即恢复main函数的rbp
  10. ret ; 把main函数的返回地址赋值给rip寄存器,下一步CPU会调到该地址的指令执行
  11. main:
  12. pushq %rbp ; main上级函数rbp指针值压入栈
  13. movq %rsp, %rbp ; 把rsp寄存器值赋值给rbp寄存器
  14. subq $16, %rsp ; rsp指针下移16,为main函数栈预留空间
  15. movl $1, -4(%rbp) ; 为变量a赋值1
  16. movl $2, -8(%rbp) ; 为变量b赋值2
  17. movl -8(%rbp), %edx
  18. movl -4(%rbp), %eax
  19. movl %edx, %esi ; 变量b赋值给esi寄存器,即第2个参数
  20. movl %eax, %edi ; 变量a赋值给edi寄存器,即第二个参数
  21. call add ; 调用add函数,此步骤会将main函数的返回地址压入栈,然后将add函数入口地址赋值给rip寄存器,下一步CPU就会执行到add函数
  22. movl %eax, -12(%rbp) ; add函数返回后,结果存放在rax寄存器中,将其赋值给变量x
  23. movl $0, %eax ; rax清零
  24. leave
  25. ret

二、什么是协程?协程与线程的关系是什么?

提到协程就不得不提线程这个概念。我们都知道线程其实就是一段子程序,一个进程就是由多个线程来组成(类似将一个任务分割为多个子任务)。由于操作系统调度的最小单位是线程,当操作系统调度到某个线程时,去执行这段子程序就行了。

然而线程的执行也不是一帆风顺的,当线程执行过程中发生了阻塞(这里主要是阻塞IO操作),那么这个线程就会一直休眠直到条件就绪才会被重新调度执行。

那有没有办法让线程不休眠呢?

可以!我们假设存在一个任务池,里面包括很多个小任务。而线程就是去执行任务池中的某几个任务,当线程在执行其中某个任务时过程中发现IO条件未就绪时,该线程可以主动跳转去执行其他的任务;而当IO条件就绪后,线程又会跳回到之前的任务继续执行。这样线程就不因为阻塞在IO上而休眠了。而这些小任务就被称为协程。

当然,从以上过程可以看出,在单个线程中,协程一定是串行执行的,不可能存在一个线程同时在执行多个协程的情况。

线程的切换是操作系统实现的,此操作是会陷入内核态的,这无疑造成了一定程度的消耗。当程序中出现了大量的线程切换时,这对系统的损耗是难以接受的。因此线程的数量需要严格控制。

而协程正好也恰好可以解决这些问题。协程可以看作是轻量级的线程,也是用户态的线程

用户态是指协程的切换完全是在用户态进行的,而不会陷入内核。协程的调度完全取决于用户的实现方式,而与操作系统的调度无关。

轻量级是指协程通常只需要一个小空间的栈就足够了,比如128k空间就足够了。因此,相比对线程而言,在操作系统中可以创建更多的协程用于多任务处理。

而且协程的切换是在用户态的,不会陷入内核,因此协程切换的开销也是相当小的。后面会讲到,协程的切换无非是改变几个寄存器的值即可。

正是因为协程的这种特性,使得协程更加适用于IO密集型的任务中,因为IO通常会伴随着大量的阻塞等待过程,而使用协程就可以在IO阻塞的同时让出CPU,而当IO就绪后再主动抢占CPU即可。

现如今,很多语言都实现了自己的协程库,如GO语言的Goroutine、C++20推出的无栈协程等等。

三、协程上下文

前面也说了协程不过是一段子程序(其实也就是个函数)罢了,因此只要保存下当前的函数栈状态、寄存器值,就可以描述出这个协程的全部状态,这个结构被称为协程上下文。

协程上下文其实就是保存了当前所有寄存器的值,这些寄存器描述了当前的函数栈,程序执行状态等信息。

  1. struct coctx_t
  2. {
  3. void *regs[ 14 ]; // 一个数组,保存了14个寄存器的值
  4. size_t ss_size; // 协程栈大小
  5. char *ss_sp; // 协程栈指针
  6. };

可以看到,在coctx_t这个结构体中。ss_size和ss_sp则描述了协程的栈空间大小。regs是一个数组,保存着14个寄存器当前的值。

四、协程切换

有了协程上下文之后,协程切换就很容易理解了。协程切换只需要两步就够了:

  1. 保存当前寄存器的值到协程上下文中的regs数组;
  2. 将新协程上下文的regs数组中值取出来赋值给对应的寄存器

完成这两步后,就切换到新的协程的函数栈上了。接下来就会在新的协程中运行了。

参考:协程篇(二)-- 协程切换篇 - 知乎

为什么协程切换的代价比线程切换低? - 知乎

与[转帖]协程切换和线程切换相似的内容:

[转帖]协程切换和线程切换

先说结论:协程切换比线程切换快主要有两点: (1)协程切换完全在用户空间进行,线程切换涉及特权模式切换,需要在内核空间完成;(2)协程切换相比线程切换做的事情更少。 协程切换只涉及基本的CPU上下文切换,所谓的 CPU 上下文,就是一堆寄存器,里面保存了 CPU运行任务所需要的信息:从哪里开始运行(

[转帖]「理解C++20协程原理」从Linux线程、线程与异步编程、协程与异步

协程不是系统级线程,很多时候协程被称为“轻量级线程”、“微线程”、“纤程(fiber)”等。简单来说可以认为协程是线程里不同的函数,这些函数之间可以相互快速切换。 协程和用户态线程非常接近,用户态线程之间的切换不需要陷入内核,但部分操作系统中用户态线程的切换需要内核态线程的辅助。 协程是编程语言(或

[转帖]总结:协程与线程

一、介绍 本文主要梳理下进程,线程,协程的概念、区别以及使用场景的选择。 二、进程 我们知道,一切的软件都是跑在操作系统上,真正用来干活 (计算) 的是 CPU。早期的操作系统每个程序就是一个进程,知道一个程序运行完,才能进行下一个进程,就是 “单进程时代”。一切的程序只能串行发生。 早期的单进程操

[转帖]进程 线程和 协程

作者:ScratchLab链接:https://www.zhihu.com/question/308641794/answer/2867920715来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 线程有两种实现方式:内核态线程和用户态线程。早期,内核态线程由于概念清晰

[转帖]eBPF 完全入门指南.pdf(万字长文)

https://zhuanlan.zhihu.com/p/492185920 图片 eBPF 源于 BPF[1],本质上是处于内核中的一个高效与灵活的虚类虚拟机组件,以一种安全的方式在许多内核 hook 点执行字节码。BPF 最初的目的是用于高效网络报文过滤,经过重新设计,eBPF 不再局限于网络协

[转帖]Docker网络解决方案-Calico部署记录

Docker网络解决方案-Calico部署记录 时间:2022-04-23 本文章向大家介绍Docker网络解决方案-Calico部署记录,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。 Calico简单简介 Calico是一个纯三层的协

[转帖]paramiko简介

https://www.cnblogs.com/qiujichu/p/12048763.html 一、什么是paramiko 要想明白什么是paramiko,要先明白ssh协议。 二、什么是ssh协议 ssh全称是Secure Shell (翻译:安全的外壳),根据字面意思就可以知道是和安全相关的协

[转帖]【Kafka】Kafka配置参数详解

Kafka配置参数详解 Kafka得安装与基本命令Kafka配置参数kafka生产者配置参数kafka消费者配置参数 本篇文章只是做一个转载的作用以方便自己的阅读,文章主要转载于: Kafka核心配置参数与机制一文 版权声明:本文为CSDN博主「张行之」的原创文章,遵循CC 4.0 BY-SA版权协

[转帖]

Linux ubuntu20.04 网络配置(图文教程) 因为我是刚装好的最小系统,所以很多东西都没有,在开始配置之前需要做下准备 环境准备 系统:ubuntu20.04网卡:双网卡 网卡一:供连接互联网使用网卡二:供连接内网使用(看情况,如果一张网卡足够,没必要做第二张网卡) 工具: net-to

[转帖]

https://cloud.tencent.com/developer/article/2168105?areaSource=104001.13&traceId=zcVNsKTUApF9rNJSkcCbB 前言 Redis作为高性能的内存数据库,在大数据量的情况下也会遇到性能瓶颈,日常开发中只有时刻