Python 的 generator 和 Go 的 goroutine 都是常用的技法。没看到有人分析其间关系,所以在此记录一下。
这两个概念都是为了 producer-consumer 模式的编程方便发明的。Python 的 generator 和 iterator 以及 iterable objects 一脉相承。Go 出现比 Python 晚,解决同样的编程便捷性问题,用 channel 和 goroutine 两个概念。
Go 的做法
Go 的做法比较容易理解,因为和教材里的概念一致:producer 和 consumer 各自是一个 goroutine,而一个 goroutine 是一种 green thread —— 自己放弃执行,让其他 gorotine 有机会占用 CPU,而不依赖一个 preemption 机制(比如 OS kernel)来强制休眠当前 thread 以腾出 CPU 给其他 thread。
producer 把数据写入一个 channel,consumer 从这个 channel 里读。一个 channel 就是一个 blocking queue,可以有一个 buffer。读可以通过 loop 语法。比如
- package main
- func producer(n int) chan int {
- ch := make(chan int)
- go func() { // This goroutine is the producer
- for i := 0; i < n; i++ {
- ch <- i
- }
- close(ch)
- }()
- return ch
- }
- func main() { // the main goroutine is the consumer
- for i := range producer(5) {
- println(i)
- }
- }
请注意,上述写法让一个 Go 函数创建和返回一个 channel,同时这个 Go 函数启动一个“发射后不管”的 producer goroutine —— 这是标准 Go 做法,不太符合 C/C++ 的习俗 —— (1)创建 channel(2)启动 producer 和 consumer threads。这是因为 C/C++ 不支持 high-order functions,或者叫 functionals。具体请参见我的这个回答 什么是函数式编程思维? 这个 Go pattern 和 Python 习俗一致,因为这俩都是 functional programming languages。
Python 的做法
上述 Go 的 producer 非常接近 Python 的 generator 的写法 —— 两点区别,都是 Python 解释器代劳的结果:
- Python 用户不需要创建和关闭 channel 了。
- ch <- i 这一行可以用 yield i 来代替。
对应的 Python generator 如下
- from typing import Iterator
- def producer(n: int) -> Iterator[int]:
- for i in range(n):
- yield i
- for i in producer(5):
- print(i)
比较
Python 的 producer 不是一个函数,因为里面没有 return,而是一个 generator,因为里面有 yield。一个函数返回一个值。而一个 generator 返回一个 iterator。
Go 的 producer 是一个函数,返回一个 channel。Go 里没有 generator 这样的“新概念”。
上面 Python generator 里的代码和 Go producer 里启动的 goroutine 的代码几乎完全一样,只是把 ch <- i 换成了 yield i。
那么 Python generator 返回的 iterator 到底是个啥呢?其实就是那个 Go channel,或者叫 blocking queue 的。从这个角度看,Python generator 又是一个函数了,返回一个 blocking queue。
Python 里最常用的 generator 莫过于 range —— 上例中也出现了。所以上例中,其实调用 range 的时候,已经创建了一个 Python thread 往 range 返回的 blocking queue 里写数字。而 producer 只是从这个 queue 里取出数字,再 yield 到 producer 创建的第二个 queue 里,让 for i in producer(5) 这一行(由 main thread 执行)去读。
这样一串三个 Python threads,通过两个 queues 连成一串,就是 Rob Pike 在著名幻灯片 https://talks.golang.org/2012/concurrency.slide#1 里展示 Go concurrency pattern 里的 pipeline:
不过这里有一个区别,goroutines 是可以并行执行的,如果我们电脑里有多个 CPU cores。不过,Python threads 虽然就是 OS thread 却受制于 Python 的 GIL,所以任何时候只有一个 Python 在执行中,即使我们有很多 CPU cores。请看https://www.zhihu.com/pin/1343421894465474560
Occam's Razor
我们设计系统的时候经常需要遵循一个哲学原则 Occam's Razor —— 能达到目的的各种手段里我们选择最简单的那个。这也是本专栏名字的由来。在汉语里,这个原则(philosophical principle)叫“删繁就简三秋树”。如果做不到,必然积累还不完的技术债,以至于不可能“领异标新二月花”。
对比上面 Go 和 Python 两个例子,显然 Python 例子的代码更简单。那么是不是就说明 Python 语言的设计比 Go 更加符合 Occam's Razor 的原则了呢?
恐怕并不尽然。虽然 Python 代码简短,但是需要用户理解更多概念(generator,iterator,以及它们和 functions 以及 queues 的潜在关系)—— 这也是一种开销。
这里只是提醒大家关注保持设计的简洁。不在于挖坑比较 Python 和 Go 语言。如果回复有涉及这样比较的,恕删。:-)