程序为了实现多任务,可以用多进程,多线程。但这两种,资源耗费较高。比如多线程,每一次切换,都会有成本。从一个线程a切到另一线程b,再切回线程a,操作系统是如何记住程序a执行位置的。其实在cpu中,有专门的寄存器存放程序执行位置。等切到线程b,系统会把线程a的执行上下文信息存在内存中。这样线程间切换就会有成本。为了解决这个问题。协程被提出来。
这里提出个问题:协程在虚拟机层面的工作原理是什么?
协程的工作原理:
我们看下面代码:
def co_process(arg):
print('task with argument {} started'.format(arg))
data = yield 1
print('step one finished, got {} from caller'.format(data))
data = yield 2
print('step two finished, got {} from caller'.format(data))
data = yield 3
print('step three finished, got {} from caller'.format(data))
genco1 = co_process('1')
genco2 = co_process('2')
genco3 = co_process('3')
while True:
next(genco1)
next(genco2)
next(genco3)
再看它们背后的字节码:
/usr/local/bin/python3.9 /Users/tanliang/PycharmProjects/python源码剖析/compile.py
1 0 LOAD_CONST 0 (<code object co_process at 0x10acdbdf0, file "circle.py", line 1>)
2 LOAD_CONST 1 ('co_process')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (co_process)13 8 LOAD_NAME 0 (co_process)
10 LOAD_CONST 2 ('1')
12 CALL_FUNCTION 1
14 STORE_NAME 1 (genco1)14 16 LOAD_NAME 0 (co_process)
18 LOAD_CONST 3 ('2')
20 CALL_FUNCTION 1
22 STORE_NAME 2 (genco2)15 24 LOAD_NAME 0 (co_process)
26 LOAD_CONST 4 ('3')
28 CALL_FUNCTION 1
30 STORE_NAME 3 (genco3)18 >> 32 LOAD_NAME 4 (next)
34 LOAD_NAME 1 (genco1)
36 CALL_FUNCTION 1
38 POP_TOP19 40 LOAD_NAME 4 (next)
42 LOAD_NAME 2 (genco2)
44 CALL_FUNCTION 1
46 POP_TOP20 48 LOAD_NAME 4 (next)
50 LOAD_NAME 3 (genco3)
52 CALL_FUNCTION 1
54 POP_TOP
56 JUMP_ABSOLUTE 32
58 LOAD_CONST 5 (None)
60 RETURN_VALUEDisassembly of <code object co_process at 0x10acdbdf0, file "circle.py", line 1>:
2 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('task with argument {} started')
4 LOAD_METHOD 1 (format)
6 LOAD_FAST 0 (arg)
8 CALL_METHOD 1
10 CALL_FUNCTION 1
12 POP_TOP4 14 LOAD_CONST 2 (1)
16 YIELD_VALUE
18 STORE_FAST 1 (data)5 20 LOAD_GLOBAL 0 (print)
22 LOAD_CONST 3 ('step one finished, got {} from caller')
24 LOAD_METHOD 1 (format)
26 LOAD_FAST 1 (data)
28 CALL_METHOD 1
30 CALL_FUNCTION 1
32 POP_TOP7 34 LOAD_CONST 4 (2)
36 YIELD_VALUE
38 STORE_FAST 1 (data)8 40 LOAD_GLOBAL 0 (print)
42 LOAD_CONST 5 ('step two finished, got {} from caller')
44 LOAD_METHOD 1 (format)
46 LOAD_FAST 1 (data)
48 CALL_METHOD 1
50 CALL_FUNCTION 1
52 POP_TOP10 54 LOAD_CONST 6 (3)
56 YIELD_VALUE
58 STORE_FAST 1 (data)11 60 LOAD_GLOBAL 0 (print)
62 LOAD_CONST 7 ('step three finished, got {} from caller')
64 LOAD_METHOD 1 (format)
66 LOAD_FAST 1 (data)
68 CALL_METHOD 1
70 CALL_FUNCTION 1
72 POP_TOP
74 LOAD_CONST 0 (None)
76 RETURN_VALUE
NoneProcess finished with exit code 0
分析字节码:
第1行:定义一个函数对象:co_process。
第13-15行:调用函数对象,生成generator对象。
第18-20行:18行调用next函数,进入栈帧对象。在栈帧对象中,执行栈帧对象的代码对象。当运行到YIELD_VALUE,会把相应值返回,然后退出当前栈帧对象,回到模块栈帧。这里控制权回到模块。现在执行19行,再次把相应栈帧压入虚拟机。然后执行函数代码对象,当执行到YIELD_VALUE,同样返回值 ,把控制权回到虚拟机模块级别。所以执行就在各自的生成器对象和模块间切换。实现了多任务的功能。
虚拟机通过一个while循环,重新回到18行执行时,为什么能接着第5行代码执行,好像有记忆功能似的?
其实很好实现,每个生成器对象都有一个栈帧对象,而栈帧对象维护着一个变量记录当前执行代码的位置。所以当代码重新回到18行时,再次把之前生成器栈帧压入当前栈帧,读取代码执行位置,就能接着第5行代码执行。