从generator的send接口参数看python的异步机制

一、generator的生成

当解析一个函数的时候,在词法分析阶段,如果发现函数中使用了yield的指令,则会将函数设置为一个特殊的CO_GENERATOR标志,表示这个函数是一个生成。这个标志对于生成器的识别非常重要,因为在对一个函数执行CALLFUNCTION的时候,判断如果一个函数有这个标志,则并不会真正的执行这个函数,而是在这个函数的基础上生成一个新的generator函数。这个步骤对于生成generator来说至关重要,因为对于常规函数执行CALLFUNCTION指令,它每次在虚拟机内部都生成一个新的PyFrameObject对象实例,这种缺省的行为对于generator来说是不能接收的,因为generator/coroutine要保留上次执行时的栈帧信息,从而可以再次执行的时候可以完美继续。
相反,当我们使用一个generator/coroutine来封装这个栈帧的时候,这个封装类就可以任意的操作这个保留有上下文的PyFrameObject对象,而这个正是generator/coroutine所必须的实现功能。

1、执行CALLFUNCTION虚拟机指令时解释器的特殊处理

下面是在执行CALLFUNCTION虚拟机指令时对于CO_GENERATOR的处理逻辑
static PyObject *
_PyEval_EvalCodeWithName(PyObject *_co, PyObject *globals, PyObject *locals,
PyObject **args, Py_ssize_t argcount,
PyObject **kwnames, PyObject **kwargs,
Py_ssize_t kwcount, int kwstep,
PyObject **defs, Py_ssize_t defcount,
PyObject *kwdefs, PyObject *closure,
PyObject *name, PyObject *qualname)
{
……
/* Handle generator/coroutine/asynchronous generator */
if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) {
PyObject *gen;
PyObject *coro_wrapper = tstate->coroutine_wrapper;
int is_coro = co->co_flags & CO_COROUTINE;

if (is_coro && tstate->in_coroutine_wrapper) {
assert(coro_wrapper != NULL);
PyErr_Format(PyExc_RuntimeError,
"coroutine wrapper %.200R attempted "
"to recursively wrap %.200R",
coro_wrapper,
co);
goto fail;
}

/* Don't need to keep the reference to f_back, it will be set
* when the generator is resumed. */
Py_CLEAR(f->f_back);

PCALL(PCALL_GENERATOR);

/* Create a new generator that owns the ready to run frame
* and return that as the value. */
if (is_coro) {
gen = PyCoro_New(f, name, qualname);
} else if (co->co_flags & CO_ASYNC_GENERATOR) {
gen = PyAsyncGen_New(f, name, qualname);
} else {
gen = PyGen_NewWithQualName(f, name, qualname);
}
if (gen == NULL)
return NULL;

if (is_coro && coro_wrapper != NULL) {
PyObject *wrapped;
tstate->in_coroutine_wrapper = 1;
wrapped = PyObject_CallFunction(coro_wrapper, "N", gen);
tstate->in_coroutine_wrapper = 0;
return wrapped;
}

return gen;
}

retval = PyEval_EvalFrameEx(f,0);
……
}

2、从function对象到generator对象

对于基础的generator对象,其中调用PyGen_NewWithQualName(f, name, qualname)来生成一个generator对象,所以当我们调用一个函数的时候,它返回的是一个generator对象。或者再简单的说,可以认为是解析器根本没有执行函数调用,只是把函数调用封装在一个对象中返回给调用者。是不是感觉很诡异?
tsecer@harry: cat itergen.py
def geniter(x):
for i in range(x):
print(i)
yield i
print(geniter(10))
tsecer@harry: ../../Python-3.6.0/python itergen.py
<generator object geniter at 0x7fa8bea1b960>
tsecer@harry:
在python内部,generator实现了gen_iternext接口,并且在gen_methods中定义了next接口
PyTypeObject PyGen_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"generator", /* tp_name */
……
PyObject_SelfIter, /* tp_iter */
(iternextfunc)gen_iternext, /* tp_iternext */
gen_methods, /* tp_methods */
gen_memberlist, /* tp_members */
gen_getsetlist, /* tp_getset */
……
}
两个接口的定义:
static PyObject *
gen_iternext(PyGenObject *gen)
{
return gen_send_ex(gen, NULL, 0, 0);
}

PyObject *
_PyGen_Send(PyGenObject *gen, PyObject *arg)
{
return gen_send_ex(gen, arg, 0, 0);
}

二、yield对应的解释器指令

1、虚拟机指令

python脚本文件及对应的虚拟机指令
tsecer@harry: cat itersend.py
def geniter(x):
for x in range(x):
y = yield x
print(y)
iterinst = geniter(10)
dir(iterinst)
next(iterinst)
#iterinst()
print(iterinst.send(None))
geniter函数中yield对应的虚拟机指令
>>> dis.dis(itersend.geniter)
2 0 SETUP_LOOP 30 (to 32)
2 LOAD_GLOBAL 0 (range)
4 LOAD_FAST 0 (x)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 18 (to 30)
12 STORE_FAST 0 (x)

3 14 LOAD_FAST 0 (x)
16 YIELD_VALUE
18 STORE_FAST 1 (y)

4 20 LOAD_GLOBAL 1 (print)
22 LOAD_FAST 1 (y)
24 CALL_FUNCTION 1
26 POP_TOP
28 JUMP_ABSOLUTE 10
>> 30 POP_BLOCK
>> 32 LOAD_CONST 0 (None)
34 RETURN_VALUE
>>>
tsecer@harry:

2、虚拟机指令的执行

可以看到,这里主要操作是保留堆栈栈顶的指针,并且把yield指令的结果赋值给retval,然后结束虚拟机指令的继续执行。
TARGET(YIELD_VALUE) {
retval = POP();

if (co->co_flags & CO_ASYNC_GENERATOR) {
PyObject *w = _PyAsyncGenValueWrapperNew(retval);
Py_DECREF(retval);
if (w == NULL) {
retval = NULL;
goto error;
}
retval = w;
}

f->f_stacktop = stack_pointer;
why = WHY_YIELD;
goto fast_yield;
}

三、挂起之后如何继续

从前面的实现看,在generator/coroutine中都定义了send接口,该接口可以恢复函数继续执行。那么send这个接口中arg的作用是干什么用的呢?从python中对于该接口的文档描述可以知道它的大致功能为
"send(arg) -> send 'arg' into generator,\n\
return next yielded value or raise StopIteration.");
这个描述依然比较模糊,所以继续看下send接口具体执行了什么:
static PyObject *
gen_send_ex(PyGenObject *gen, PyObject *arg, int exc, int closing)
{
……
} else {
/* Push arg onto the frame's value stack */
result = arg ? arg : Py_None;
Py_INCREF(result);
*(f->f_stacktop++) = result;
}
……
}
可以看到是把参数压入了挂起Frame的堆栈顶端,而python的局部参数传递都是基于堆栈操作的。再看下前面的python代码及对应的虚拟机指令
y = yield x

y = yield x * x
那么这里生成的虚拟机指令
3 14 LOAD_FAST 0 (x)
16 YIELD_VALUE
18 STORE_FAST 1 (y)
在执行STORE_FAST的动作
PREDICTED(STORE_FAST);
TARGET(STORE_FAST) {
PyObject *value = POP();
SETLOCAL(oparg, value);
FAST_DISPATCH();
}
首先从从栈顶取出数值,而这个值就是send(arg)压入的arg对象,也就是y的值被替换为了arg。
下面代码的执行结果
tsecer@harry: cat itersend.py
def geniter(x):
for x in range(x):
y = yield x
print(y)
iterinst = geniter(10)
dir(iterinst)
next(iterinst)
#iterinst()
print(iterinst.send(None))
tsecer@harry: ../../Python-3.6.0/python itersend.py
None
1
tsecer@harry:

原文地址:https://www.cnblogs.com/tsecer/p/10419533.html