greenlet是stackless

Python中剥离出来的一个项目,可以作为官方CPython的一个扩展来使用,从而支持Python协程。gevent正是基于greenlet实现。

协程实现原理

实现协程主要是在协程切换时,将协程当前的执行上下文保存到协程关联的context中。在c/c++这种native程序中实现协程,需要将栈内容和CPU各个寄存器的内容保存起来。在Python这种VM中则有些不同。例如,在以下基于greenlet协程的python程序中:

1

2

3

4

5

6

7

8

9

10

11

12

13def foo():bar()def bar():a = 3 + 1gr2.switch()def func():passgr1 = greenlet(foo)gr2 = greenlet(func)gr1.switch()

在bar中gr2.switch切换到gr2时,协程库需要保存gr1协程的执行上下文。这个上下文包括:

Python VM的stack

Python VM中解释执行的上下文

理解以上两点非常重要,至于为什么呢?想象一下如何去实现一个Python

VM,去解释执行一段Python代码。其实这在任何基于VM的语言中,原理都是一样的(native程序可以把x86物理CPU也视作特殊的VM)。可以参考Python解释器简介-深入主循环。主要包含两方面内容:

VM在执行代码时,其自身调用栈通常都是递归的

VM在执行代码时,通常会创建相应的数据结构来表示代码执行块,例如通常会有个struct Frame来表示一个函数

在VM的实现中通常会有类似以下的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16struct Frame {unsigned char *codes; // 存放代码指令size_t pc; // 当前执行的指令位置int *stack; // stack-based的VM会有一个栈用于存放指令操作数};void op_call(frame) {switch (OP_CODE()) {case OP_CALL:child_frame = new_frame()op_call(child_frame)...case OP_ADD:op_add(...)}}

对应到前面的Python例子代码,在某一时刻VM的call stack可能是这样的:

1

2

3op_addop_callop_call

理解了以上内容后,就可以推测出greenlet本质上也是做了以上两件事。

greenlet实现原理

greenlet库中每一个协程称为一个greenlet。greenlet都有一个栈空间,如下图:

图中未表达出来的,greenlet的栈空间地址可能是重叠的。对于活跃的(当前正在运行)的greenlet,其栈内容必然在c程序栈顶。而不活跃的被切走的greenlet,其栈内容会被copy到新分配的堆内存中。greenlet的栈空间是动态的,其起始地址是固定的,但栈顶地址不固定。以下代码展示一个greenlet的栈空间如何确定:

1

2

3

4579 if (!PyGreenlet_STARTED(target)) { // greenlet未启动,是一个需要新创建的greenlet580 void* dummymarker; // 该局部变量的地址成为新的greenlet的栈底581 ts_target = target;582 err = g_initialstub(&dummymarker); // 创建该greenlet并运行

以上greenlet->stack_stop确定了栈底,而栈顶则是动态的,在切换到其他greenlet前,对当前greenlet进行上下文的保存时,获取当前的RSP(程序实际运行的栈顶地址):

1

2

3

4

5

6

7

8

9

10410 static int GREENLET_NOINLINE(slp_save_state)(char* stackref)411 {412 /* must free all the C stack up to target_stop */413 char* target_stop = ts_target->stack_stop;414 PyGreenlet* owner = ts_current;415 assert(owner->stack_saved == 0);416 if (owner->stack_start == NULL)417 owner = owner->stack_prev; /* not saved if dying */418 else419 owner->stack_start = stackref; // stack_start指向栈顶

stackref是通过汇编获取当前RSP寄存器的值:

1__asm__ ("movl %%esp, %0" : "=g" (stackref));

保存栈内容到堆内存参看g_save的实现,没什么特别的。除了保存栈内容外,如上一节讲的,还需要保存VM执行函数所对应的Frame对象,这个在g_switchstack中体现:

1

2

3

4

5

6

7

8

9460 PyThreadState* tstate = PyThreadState_GET(); // 获取当前线程的VM执行上下文461 current->recursion_depth = tstate->recursion_depth;462 current->top_frame = tstate->frame; // 保存当前正在执行的frame到当前正在执行的greenlet...473 slp_switch(); // 做栈切换...487 PyThreadState* tstate = PyThreadState_GET();488 tstate->recursion_depth = target->recursion_depth;489 tstate->frame = target->top_frame; // 切换回来

上面的代码展示VM frame的切换。接下来看下最复杂的部分,当切换到目标greenlet时,如何恢复目标greenlet的执行上下文,这里主要就是恢复目标greenlet的栈空间。假设有如下greenlet应用代码:

1

2

3

4

5

6

7

8

9def test1():gr2.switch()def test2():print('test2')gr1 = greenlet(test1)gr2 = greenlet(test2)gr1.switch()

在gr1中切换到gr2时,也就是gr2.switch,会发生什么事情。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33// g_switch 实现574 if (PyGreenlet_ACTIVE(target)) {575 ts_target = target; // 找到目标greenlet,也就是gr2576 err = g_switchstack(); // 开始切换// g_switchstack 实现462 current->top_frame = tstate->frame;...473 err = slp_switch();// slp_switch 实现,根据不同平台实现方式不同,原理相同69 SLP_SAVE_STATE(stackref, stsizediff);// 这个很重要,强行将当前的栈指针ESP/EBP (32位OS)通过加上一个与目标greenlet栈地址的偏移,而回到了// 目标greenlet的栈空间。可以在下文看到stsizediff的获取实现70 __asm__ volatile (71 "addl %0, %%esp\n"72 "addl %0, %%ebp\n"73 :74 : "r" (stsizediff)75 );76 SLP_RESTORE_STATE();// SLP_SAVE_STATE 实现316 #define SLP_SAVE_STATE(stackref, stsizediff) \317 stackref += STACK_MAGIC; \318 if (slp_save_state((char*)stackref)) return -1; \319 if (!PyGreenlet_ACTIVE(ts_target)) return 1; \// 获取目标greenlet的栈空间与当前栈地址的偏移,用于稍后设置当前栈地址回目标greenlet的栈地址320 stsizediff = ts_target->stack_start - (char*)stackref// slp_save_state 没啥看的,前面也提过了,主要就是复制当前greenlet栈内容到堆内存// SLP_RESTORE_STATE 也没什么看的,主要就是把greenlet堆内存复制回栈空间

以上,首先将ESP/EBP的值改回目标greenlet当初切换走时的ESP/EBP值,然后再把greenlet的栈空间内存(存放于堆内存中)全部复制回来,就实现了greenlet栈的回切。尤其注意的是,这个栈中是保存了各种函数的return地址的,所以当slp_switch返回时,就完全恢复到了目标greenlet当初被切走时栈上的内容,包括各种函数调用栈。而当前greenlet的栈,则停留在了类似以下的函数调用栈:

1

2

3g_switchstackg_switch...

参考

python 协程原理_Python协程greenlet实现原理相关推荐

  1. python 协程库_python --- 协程编程(第三方库gevent的使用)

    1. 什么是协程? 协程(coroutine),又称微线程.协程不是线程也不是进程,它的上下文关系切换不是由CPU控制,一个协程由当前任务切换到其他任务由当前任务来控制.一个线程可以包含多个协程,对于 ...

  2. python携程多核_python 协程

    最近对Python中的协程挺感兴趣,这里记录对协程的个人理解. 要理解协程,首先需要知道生成器是什么.生成器其实就是不断产出值的函数,只不过在函数中需要使用yield这一个关键词将值产出.下面来看一个 ...

  3. python 协程库_python协程概念

    什么是协程 协程是单线程下的并发,又称微线程,纤程.它是实现多任务的另一种方式,只不过是比线程更小的执行单元.因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程.英文名 ...

  4. python从零开始到放弃_Python 协程从零开始到放弃

    0x00 前言 很久以前就听说 Python 的 async/await 很厉害,但是直到现在都没有用过,一直都在用多线程模型来解决各种问题.最近看到隔壁的 Go 又很火,所以决定花时间研究下 Pyt ...

  5. python gevent模块 下载_Python协程阻塞IO非阻塞IO同步IO异步IO

    Python-协程-阻塞IO-非阻塞IO-同步IO-异步IO 一.协程 协程又称为微线程 CPU 是无法识别协程的,只能识别是线程,协程是由开发人员自己控制的.协程可以在单线程下实现并发的效果(实际计 ...

  6. python 协程库_python 协程库gevent学习--gevent数据结构及实战(四)

    一不留神已经到第四部分了,这一部分继续总结数据结构和常用的gevent类,废话不多说继续. 1.Timeout错误类 晚上在调试调用第三方接口的时候,发现有些接口耗时非常多,觉得应该有个超时接口来限制 ...

  7. python协程实时输出_python协程

    不知道你有没有被问到过有没有使用过的python协程? 协程是什么? 协程是一种用户态轻量级,是实现并发编程的一种方式.说到并发,就能想到了多线程 / 多进程模型,是解决并发问题的经典模型之一. 但是 ...

  8. python 协程库_python 协程库gevent学习--源码学习(一)

    总算还是要来梳理一下这几天深入研究之后学习到的东西了. 这几天一直在看以前跟jd对接的项目写的那个gevent代码.为了查错,基本上深入浅出了一次gevent几个重要部件的实现和其工作的原理. 这里用 ...

  9. python主辅线程_python主线程捕获子线程的方法

    最近,在做一个项目时遇到的了一个问题,主线程无法捕获子线程中抛出的异常. 先看一个线程类的定义 ''''' Created on Oct 27, 2015 @author: wujz ''' impo ...

最新文章

  1. POJ2308连连看dfs+bfs+优化
  2. JAVA排序算法之希尔排序
  3. HarmonyOS之AI能力·词性标注
  4. Python 爬虫利器一之 Requests 库的用法
  5. 管理数据库计算机网络,计算机网络与数据库管理系统.pdf
  6. 超出部分用省略号代替的样式
  7. PHPMailer 报错:SMTP ERROR: Password command failed: 535 Login Fail
  8. 开源点云数据处理 开源_大数据开源安全
  9. silverlight 初始页面进行安装主应用(初始安装xap,本地加载xap),实现silverlight程序成桌面应用程序。
  10. 密码学基础之对称密钥的分发和存储
  11. PPT文字怎样做断开效果和穿透效果
  12. ESET NOD32最新版本的安装与激活
  13. 2021年12月电子学会图形化四级编程题解析含答案:棕熊大战
  14. 深入探讨 Room 2.4.0 的最新进展
  15. python多轴图_python中用Matplotlib做多个纵轴 (多y轴)
  16. 激光导引头电子舱测试系统软件的研究与开发
  17. MVNO忽略国内漫游(ignore national roaming)
  18. javaWEB——主页面新闻展示删除查看修改主题绑定
  19. Win7开启无线热点AP
  20. Nylg541 最强DE 战斗力

热门文章

  1. 一文读懂残差网络ResNet
  2. LeNet试验(一) 搭建pytorch版模型及运行
  3. AAAI21最佳论文Informer:效果远超Transformer的长序列预测神器!
  4. 转载 MySQL 性能优化的最佳20多条经验分享 http://www.jb51.net/article/24392.htm
  5. webstorm和intellij idea下如何自动编译sass和scss文件
  6. 【机器学习课程01】李宏毅2020年机器学习课程开课啦!!!
  7. 和功率的计算公式_电机电流的计算公式是什么,具体怎么计算?
  8. 【OS】期末总结复习
  9. 推荐 7 个 Github 上近 200k Star 的计算机学习资源,练好前端内功的秘籍!
  10. Jenkins构建项目,JAVA_HOME is not defined correctly