程序运行的入口点
一般我们初次接触编程,写的顺序执行模型的程序的入口点只有 main 函数。
从 main 函数开始,按照顺序执行每一个函数。
如果没有任何异步模型的话,好比一个糖葫芦串一样,从头到尾吃到最后
把视野缩小到一个函数体——每一个进程在操作系统里,可以看成一个运行实体,本质上,只要愿意,进程的行为实际上都可以在一个函数体内实现——大体上如此,所以我们讨论入口点的时候可以把视野先放到一个函数体内。
函数的执行,需要告诉执行器入口点的位置,一般是一个函数名,在 C 世界,函数名对应了函数指针,内存中有一个独一无二的地址。
除此以外,类似 goto 这种跳转,能够为程序“开辟”一个跳转到异端执行逻辑的入口
程序的入口点为何如此重要
把函数看成一个积木,它只有首位两端可以连接,那么我们构建一个执行流,毫无疑问只能像糖葫芦串那样一个一个首位相接,它会变成一个长长的串。
我们假定这个串有严格的先后顺序,很容易将它对应到一个时间流里,这就是我们最初接触到的串行编程模型。
因为 CPU 的享用是独占式的,一旦执行流在糖葫芦串的前面,后面的执行流只能先等待。有一些任务如文件读取,TCP 数据读写,程序会“陷入”内核,程序会进入一种暂停等待的状态,空余的 CPU 继续闲着不是一个好的模型,于是人们很早的时候就想到了并发。
Go 语言发明者之一 Rob Pike 的教育: 并发不是并行(Concurrency is not Parallelism)
下面两个执行流分别代表串行和并发模型
串行流 并发流
可以看到并发模型能将三个任务的整体前置时间线前移。
问题是
- 当我们开启计算任务组task1 和 task2 之后,怎么在该任务未完成的状态下又“钻到”另一个任务的入口去?
- 当我们完成下载任务后,怎么回到也已经开启的任务 tcp流中去。
这就需要一种不限于从函数调用的方式开启入口点的方式才能支持。
方式有数种
- 一个是进程。多进程本就是操作系统支持并发流的方式。
- 另一个是线程。一个线程对应一个新的入口点,它和主线程没有主次之分,如果你不作任何同步,几个线程开启,各自如野马狂奔。
- 还有一个就是协程。也是本文讨论 yield 表达式牵连到的主题
yield 和协程
协程和进程最重要的区别可能是,进程的调度是被动的,默认情形下它很机械的向前执行,它的调度完全依赖外部的机制,线程也是类似的。而协程是主动“停下来”,让出CPU的控制权。
当然协程停下来不是让自己“死去”,而是钻进了一个类似太空冷藏室的地方,它的肌肉,细胞,记忆等等内部组织都决定暂停的时候封存起来——即内部变量,栈帧等等状态会原封不动存储——等待下一次激活。
激活之后,协程会继承之前的状态继续运行下去,知道下一次暂停或结束。
设想孙悟空定住摘蟠桃的七仙女,大圣吃完桃子之后,七仙女解封,继续摘桃子,七仙女的行为类似一个协程的暂停,虽然她不是自己主动暂停的,协程通常是自己主动停下来——即停下来的逻辑放在执行流里。
在Python 能支持这个控制的表达式就是 yield 以及 yield from (3.4.2)
yield 机制
例子——求移动平均
def average():
total = 0
n = 0
average = None
while True:
number = yield average
total += number
n += 1
average = total // n
$ gen = average()
$ next(gen)
$ gen.send(100)
yield 一旦出现,把函数变成一个生成器,调用 gen = average() 不会运行 average内部的逻辑,只是起到一个声明的作用
打印 type(gen) 会输出 <generator object average at 0x109844820>
next 内建函数可以激活一个生成器
运行到 yield 后面的表达式处为止,生成器挂起,让出 CPU 控制权。
yield 除了挂起的作用,还有一个作用是它留了一个"入口点" 让别的调用方可以通过某种手段找到并重新激活它,这个方式就是 send函数。
gen.send(value) value 会从入口点送到生成器挂起的地方,并赋值给 变量 number ,并重新激活生成器。
整个过程,我们做一个类比。
有两个人在守门值班,A 困了以后,打了一个电话给 B,说该你了,并且给 B 一个值班日志,B拿到值班日志继续值班,A 睡觉去了,直到下一次轮班,A再把 B 叫醒,递给他一份更新过得值班日志,A继续值班。
<value> = yield <expr> 右边可以跟一个表达式,表达式的计算完之后会带出去给调用方,左边是一个变量,它可以接收调用方传入的一个值
yield 使得函数的入口点可以突破常规,从而给了一种实现协程机制的可能性。
- yield 可以单独出现,既不接受调用方的任何东西,也不吐出任何东西。单独的 yield 出现时,这样说也不是特别地确切,因为实际上,它还是向外传递了 None ,如果认为 None 不是个东西的话,这么说也是没什么问题的。
- 当然 yield 也可以只含有右边的表达式,这意味着外部给的任何东西都会被忽略掉。这时候调用方传给 send() 什么参数都不重要,他只是起到一个重新激活的作用。
可见,yield 最重要的作用是流程控制
yield 和生成器
一般来说,生成器几乎就是一个协程,但是还是有区别,在 Python 2.5 版本之前, yield 的作用还不曾延及到左边那一半,只有一个右边放一个表达式这种用法,截止到 Python2.4 yield 是完全和生成器绑定在一起。
Python2.5 引入了左边的表示,并新增了诸如 send, next 以及 throw close 等 API,可以看做是Python协程最初的雏形, 这些东西在 PEP342 描述
网友评论