Python由于众所周知的GIL的原因,导致其线程无法发挥多核的并行计算能力(当然,后来有了multiprocessing,可以实现多进程并行),显得比较鸡肋。既然在GIL之下,同一时刻只能有一个线程在运行,那么对于CPU密集的程序来说,线程之间的切换开销就成了拖累,而以I/O为瓶颈的程序正是协程所擅长的:
多任务并发(非并行),每个任务在合适的时候挂起(发起I/O)和恢复(I/O结束)
Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
- 最初的生成器变形yield/send
- 引入@asyncio.coroutine和yield from
- 在最近的Python3.5版本中引入async/await关键字
从yield说起
先看一段普通的计算斐波那契续列的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def old_fib(n): res = [0] * n index = 0 a = 0 b = 1 while index res[index] = b a, b = b, a + b index += 1 return res print('-'*10 + 'test old fib' + '-'*10) for fib_res in old_fib(20): print(fib_res) |
如果我们仅仅是需要拿到斐波那契序列的第n位,或者仅仅是希望依此产生斐波那契序列,那么上面这种传统方式就会比较耗费内存。
这时,yield就派上用场了。
1 2 3 4 5 6 7 8 9 10 11 12 |
def fib(n): index = 0 a = 0 b = 1 while index yield b a, b = b, a + b index += 1 print('-'*10 + 'test yield fib' + '-'*10) for fib_res in fib(20): print(fib_res) |
当一个函数中包含yield语句时,python会自动将其识别为一个生成器。这时fib(20)并不会真正调用函数体,而是以函数体生成了一个生成器对象实例。
yield在这里可以保留fib函数的计算现场,暂停fib的计算并将b返回。而将fib放入for…in循环中时,每次循环都会调用next(fib(20)),唤醒生成器,执行到下一个yield语句处,直到抛出StopIteration异常。此异常会被for循环捕获,导致跳出循环。
Send来了
从上面的程序中可以看到,目前只有数据从fib(20)中通过yield流向外面的for循环;如果可以向fib(20)发送数据,那不是就可以在Python中实现协程了嘛。
于是,Python中的生成器有了send函数,yield表达式也拥有了返回值。
我们用这个特性,模拟一个额慢速斐波那契数列的计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
def stupid_fib(n): index = 0 a = 0 b = 1 while index sleep_cnt = yield b print('let me think {0} secs'.format(sleep_cnt)) time.sleep(sleep_cnt) a, b = b, a + b index += 1 print('-'*10 + 'test yield send' + '-'*10) N = 20 sfib = stupid_fib(N) fib_res = next(sfib) while True: print(fib_res) try: fib_res = sfib.send(random.uniform(0, 0.5)) except StopIteration: break |
其中next(sfib)相当于sfib.send(None),可以使得sfib运行至第一个yield处返回。后续的sfib.send(random.uniform(0, 0.5))则将一个随机的秒数发送给sfib,作为当前中断的yield表达式的返回值。这样,我们可以从“主”程序中控制协程计算斐波那契数列时的思考时间,协程可以返回给“主”程序计算结果,Perfect!
yield from是个什么鬼?
yield from用于重构生成器,简单的,可以这么使用:
1 2 3 4 5 6 7 |
def copy_fib(n): print('I am copy from fib') yield from fib(n) print('Copy end') print('-'*10 来有了multiprocessing,可以实现多进程并行),显得比较鸡肋。既然在GIL之下,同一时刻只能有一个线程在运行,那么对于CPU密集的程序来说,线程之间的切换开销就成了拖累,而以I/O为瓶颈的程序正是协程所擅长的:
多任务并发(非并行),每个任务在合适的时候挂起(发起I/O)和恢复(I/O结束) Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
从yield说起先看一段普通的计算斐波那契续列的代码:
如果我们仅仅是需要拿到斐波那契序列的第n位,或者仅仅是希望依此产生斐波那契序列,那么上面这种传统方式就会比较耗费内存。 这时,yield就派上用场了。
当一个函数中包含yield语句时,python会自动将其识别为一个生成器。这时fib(20)并不会真正调用函数体,而是以函数体生成了一个生成器对象实例。 yield在这里可以保留fib函数的计算现场,暂停fib的计算并将b返回。而将fib放入for…in循环中时,每次循环都会调用next(fib(20)),唤醒生成器,执行到下一个yield语句处,直到抛出StopIteration异常。此异常会被for循环捕获,导致跳出循环。 Send来了从上面的程序中可以看到,目前只有数据从fib(20)中通过yield流向外面的for循环;如果可以向fib(20)发送数据,那不是就可以在Python中实现协程了嘛。 于是,Python中的生成器有了send函数,yield表达式也拥有了返回值。 我们用这个特性,模拟一个额慢速斐波那契数列的计算:
其中next(sfib)相当于sfib.send(None),可以使得sfib运行至第一个yield处返回。后续的sfib.send(random.uniform(0, 0.5))则将一个随机的秒数发送给sfib,作为当前中断的yield表达式的返回值。这样,我们可以从“主”程序中控制协程计算斐波那契数列时的思考时间,协程可以返回给“主”程序计算结果,Perfect! yield from是个什么鬼?yield from用于重构生成器,简单的,可以这么使用:
|