数字营销 · Web开发 · 基础设施

协程与yield

协程与yield有什么关系?本文通过Python和PHP示例,介绍协程、生成器与yield的基本原理,以及协作式调度和协程的工作方式。

协程,又称微线程或纤程,是一种比进程和线程更加轻量级的执行方式,现代编程语言中也有多种不同的实现方式。那么,协程究竟有什么特点,它又是如何运作的呢?

协议与Yield

进程和线程之间的关系,我在《线程和进程的比较之我见》中做过简单比较。线程可以理解为程序的一条执行流,而进程则是资源和线程的容器。在单核 CPU 上,从某一个瞬间来看,通常只有一条线程正在执行,操作系统通过调度机制在不同的进程和线程之间进行切换。

而协程通常运行在用户空间,操作系统并不会直接对每一个协程进行调度,而是由程序自身负责协程之间的切换。相比通过操作系统内核进行线程切换,协程切换通常更加轻量,这也是协程能够高效处理大量任务的一个重要原因。

那么,协程究竟是一种什么样的存在呢?

上面说到,线程是一条指令的执行流,而这个执行流通常由函数这些最小的执行单元组成。普通情况下,一个函数执行过程中发生阻塞,后续逻辑通常需要等待它继续执行或返回。协程提供了一种不同的方式:一个函数可以在执行过程中主动暂停,保存当前状态,让出执行权,之后再从原来的位置继续恢复执行,从而实现多个任务之间的协作式调度。

Python 和 PHP 5.5 及以上版本中,可以利用生成器中的 yield 在一定程度上实现类似协程的协作式执行。例如,在一个车流量较多的路口,摄像头获取车牌号,并将结果保存到文本文件,可以这样处理:

#!/usr/bin/env python
import random

def getNum(action):
action.next()
while True:
num = random.randint(1000, 9999) # 获取车牌号,这里随机模拟
stored = action.send(num)
if stored > 500:
break
action.close()

def storage():
r = 0
while True:
f = open("./car.txt", "a")
num = yield r
print num
if num:
f.write(str(num) + "\n")
r = r + 1
f.close()

if __name__ == "__main__":
S = storage()
getNum(S)

PHP 中也可以使用 yield 实现类似的协作过程:

<?php
function getNum($action){
while(1){
$num = rand(1000, 9999);
$stored = $action->send($num);
if($stored > 500){
break;
}
}
}

function storage(){
$r = 0;
while(1){
$num = (yield $r);
if($num){
echo $num."\n";
file_put_contents("./car.txt", $num."\n", FILE_APPEND);
$r++;
}
}
}

$S = storage();
getNum($S);

yield 这个关键字在很多语言中都有不同形式的应用。在 Python 和 PHP 中,如果一个函数中包含 yield,通常会返回一个**生成器(Generator)**对象。

上面的例子中,两个函数并不是一个执行完之后,另一个才开始运行,而是通过 yieldsend() 在不同的执行阶段相互协作、交换数据。因此,这种方式更接近协作式调度,而不是线程的抢占式多任务。

需要注意的是,Python 2 中的生成器通常需要通过 next() 进行启动,然后才能向生成器发送数据;而不同语言和版本对于生成器的具体使用方式也有所区别。

当然,yield 本身并不等同于完整的协程系统,它只是提供了一种暂停和恢复执行状态的机制。要进一步理解和使用协程,还可以了解 gevent 等库。gevent 为 Python 提供了较完整的协程支持,可以通过协作式调度处理大量 I/O 密集型任务。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。