内容简介:由于GIL的存在,导致Python多线程性能甚至比单线程更糟。GIL: 全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。[1]即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。于是出现了协程(Coroutine)这么个东西。
由于GIL的存在,导致 Python 多线程性能甚至比单线程更糟。
GIL: 全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。[1]即便在多核心处理器上,使用 GIL 的解释器也只允许同一时间执行一个线程。
于是出现了协程(Coroutine)这么个东西。
协程: 协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行.
协程由于由程序主动控制切换,没有线程切换的开销,所以执行效率极高。对于IO密集型任务非常适用,如果是cpu密集型,推荐多进程+协程的方式。
在Python3.4之前,官方没有对协程的支持,存在一些三方库的实现,比如gevent和Tornado。3.4之后就内置了asyncio标准库,官方真正实现了协程这一特性。
而Python对协程的支持,是通过Generator实现的,协程是遵循某些规则的生成器。因此,我们在了解协程之前,我们先要学习生成器。
生成器(Generator)
我们这里主要讨论 yield
和 yield from
这两个表达式,这两个表达式和协程的实现息息相关。
- Python2.5中引入
yield
表达式,参见PEP342 - Python3.3中增加
yield from
语法,参见PEP380,
方法中包含 yield
表达式后,Python会将其视作generator对象,不再是普通的方法。
yield
表达式的使用
我们先来看该表达式的具体使用:
def test(): print("generator start") n = 1 while True: yield_expression_value = yield n print("yield_expression_value = %d" % yield_expression_value) n += 1 # ①创建generator对象 generator = test() print(type(generator)) print("\n---------------\n") # ②启动generator next_result = generator.__next__() print("next_result = %d" % next_result) print("\n---------------\n") # ③发送值给yield表达式 send_result = generator.send(666) print("send_result = %d" % send_result) 复制代码
执行结果:
<class 'generator'> --------------- generator start next_result = 1 --------------- yield_expression_value = 666 send_result = 2 复制代码
方法说明:
-
__next__()
方法: 作用是启动或者恢复generator的执行,相当于send(None) -
send(value)
方法:作用是发送值给yield表达式。启动generator则是调用send(None)
执行结果的说明:
-
①创建generator对象:包含yield表达式的函数将不再是一个函数,调用之后将会返回generator对象
-
②启动generator:使用生成器之前需要先调用
__next__
或者send(None)
,否则将报错。启动generator后,代码将执行到yield
出现的位置,也就是执行到yield n
,然后将n传递到generator.__next__()
这行的返回值。(注意,生成器执行到yield n
后将暂停在这里,直到下一次生成器被启动) -
③发送值给yield表达式:调用send方法可以发送值给yield表达式,同时恢复生成器的执行。生成器从上次中断的位置继续向下执行,然后遇到下一个
yield
,生成器再次暂停,切换到主函数打印出send_result。
理解这个demo的关键是:生成器启动或恢复执行一次,将会在 yield
处暂停。上面的第②步仅仅执行到了 yield n
,并没有执行到赋值语句,到了第③步,生成器恢复执行才给 yield_expression_value
赋值。
生产者和消费者模型
上面的例子中,代码中断-->切换执行,体现出了协程的部分特点。
我们再举一个生产者、消费者的例子,这个例子来自廖雪峰的Python教程:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
现在改用协程,生产者生产消息后,直接通过 yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
def consumer(): print("[CONSUMER] start") r = 'start' while True: n = yield r if not n: print("n is empty") continue print("[CONSUMER] Consumer is consuming %s" % n) r = "200 ok" def producer(c): # 启动generator start_value = c.send(None) print(start_value) n = 0 while n < 3: n += 1 print("[PRODUCER] Producer is producing %d" % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) # 关闭generator c.close() # 创建生成器 c = consumer() # 传入generator producer(c) 复制代码
执行结果:
[CONSUMER] start start [PRODUCER] producer is producing 1 [CONSUMER] consumer is consuming 1 [PRODUCER] Consumer return: 200 ok [PRODUCER] producer is producing 2 [CONSUMER] consumer is consuming 2 [PRODUCER] Consumer return: 200 ok [PRODUCER] producer is producing 3 [CONSUMER] consumer is consuming 3 [PRODUCER] Consumer return: 200 ok 复制代码
注意到 consumer
函数是一个 generator
,把一个 consumer
传入 produce
后:
- 首先调用
c.send(None)
启动生成器;
- 然后,一旦生产了东西,通过
c.send(n)
切换到consumer执行;
-
consumer
通过yield
拿到消息,处理,又通过yield
把结果传回;
-
produce
拿到consumer
处理的结果,继续生产下一条消息;
-
produce
决定不生产了,通过c.close()
关闭consumer
,整个过程结束。
整个流程无锁,由一个线程执行, produce
和 consumer
协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
yield from
表达式
Python3.3版本新增 yield from
语法,新语法用于将一个生成器部分操作委托给另一个生成器。此外,允许子生成器(即yield from后的“参数”)返回一个值,该值可供委派生成器(即包含yield from的生成器)使用。并且在委派生成器中,可对子生成器进行优化。
我们先来看最简单的应用,例如:
# 子生成器 def test(n): i = 0 while i < n: yield i i += 1 # 委派生成器 def test_yield_from(n): print("test_yield_from start") yield from test(n) print("test_yield_from end") for i in test_yield_from(3): print(i) 复制代码
输出:
test_yield_from start 0 1 2 test_yield_from end 复制代码
这里我们仅仅给这个生成器添加了一些打印,如果是正式的代码中,你可以添加正常的执行逻辑。
如果上面的 test_yield_from
函数中有两个 yield from
语句,将串行执行。比如将上面的 test_yield_from
函数改写成这样:
def test_yield_from(n): print("test_yield_from start") yield from test(n) print("test_yield_from doing") yield from test(n) print("test_yield_from end") 复制代码
将输出:
test_yield_from start 0 1 2 test_yield_from doing 0 1 2 test_yield_from end 复制代码
在这里, yield from
起到的作用相当于下面写法的简写形式
for item in test(n): yield item 复制代码
看起来这个 yield from
也没做什么大不了的事,其实它还帮我们处理了异常之类的。具体可以看stackoverflow上的这个问题: In practice, what are the main uses for the new “yield from” syntax in Python 3.3?
协程(Coroutine)
- Python3.4开始,新增了asyncio相关的API,语法使用
@asyncio.coroutine
和yield from
实现协程 - Python3.5中引入
async
/await
语法,参见PEP492
我们先来看Python3.4的实现。
@asyncio.coroutine
Python3.4中,使用 @asyncio.coroutine
装饰的函数称为协程。不过没有从语法层面进行严格约束。
对装饰器不了解的小伙伴可以看我的上一篇博客--《理解Python装饰器》
对于Python原生支持的协程来说,Python对协程和生成器做了一些区分,便于消除这两个不同但相关的概念的歧义:
- 标记了
@asyncio.coroutine
装饰器的函数称为协程函数,iscoroutinefunction()
方法返回True - 调用协程函数返回的对象称为协程对象,
iscoroutine()
函数返回True
举个栗子,我们给上面 yield from
的demo中添加 @asyncio.coroutine
:
import asyncio ... @asyncio.coroutine def test_yield_from(n): ... # 是否是协程函数 print(asyncio.iscoroutinefunction(test_yield_from)) # 是否是协程对象 print(asyncio.iscoroutine(test_yield_from(3))) 复制代码
毫无疑问输出结果是True。
可以看下 @asyncio.coroutine
的源码中查看其做了什么,我将其源码简化下,大致如下:
import functools import types import inspect def coroutine(func): # 判断是否是生成器 if inspect.isgeneratorfunction(func): coro = func else: # 将普通函数变成generator @functools.wraps(func) def coro(*args, **kw): res = func(*args, **kw) res = yield from res return res # 将generator转换成coroutine wrapper = types.coroutine(coro) # For iscoroutinefunction(). wrapper._is_coroutine = True return wrapper 复制代码
将这个装饰器标记在一个生成器上,就会将其转换成coroutine。
然后,我们来实际使用下 @asyncio.coroutine
和 yield from
:
import asyncio @asyncio.coroutine def compute(x, y): print("Compute %s + %s ..." % (x, y)) yield from asyncio.sleep(1.0) return x + y @asyncio.coroutine def print_sum(x, y): result = yield from compute(x, y) print("%s + %s = %s" % (x, y, result)) loop = asyncio.get_event_loop() print("start") # 中断调用,直到协程执行结束 loop.run_until_complete(print_sum(1, 2)) print("end") loop.close() 复制代码
执行结果:
start Compute 1 + 2 ... 1 + 2 = 3 end 复制代码
print_sum
这个协程中调用了子协程 compute
,它将等待 compute
执行结束才返回结果。
这个demo点调用流程如下图:
EventLoop将会把 print_sum
封装成Task对象
流程图展示了这个demo的控制流程,不过没有展示其全部细节。比如其中“暂停”的1s,实际上创建了一个future对象, 然后通过 BaseEventLoop.call_later()
在1s后唤醒这个任务。
值得注意的是, @asyncio.coroutine
将在Python3.10版本中移除。
async
/ await
Python3.5开始引入 async
/ await
语法(PEP 492),用来简化协程的使用并且便于理解。
async
/ await
实际上只是 @asyncio.coroutine
和 yield from
的语法糖:
- 把
@asyncio.coroutine
替换为async
- 把
yield from
替换为await
即可。
比如上面的例子:
import asyncio async def compute(x, y): print("Compute %s + %s ..." % (x, y)) await asyncio.sleep(1.0) return x + y async def print_sum(x, y): result = await compute(x, y) print("%s + %s = %s" % (x, y, result)) loop = asyncio.get_event_loop() print("start") loop.run_until_complete(print_sum(1, 2)) print("end") loop.close() 复制代码
我们再来看一个asyncio中Future的例子:
import asyncio future = asyncio.Future() async def coro1(): print("wait 1 second") await asyncio.sleep(1) print("set_result") future.set_result('data') async def coro2(): result = await future print(result) loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait([ coro1() coro2() ])) loop.close() 复制代码
输出结果:
wait 1 second (大约等待1秒) set_result data 复制代码
这里await后面跟随的future对象,协程中yield from或者await后面可以调用future对象,其作用是:暂停协程,直到future执行结束或者返回result或抛出异常。
而在我们的例子中, await future
必须要等待 future.set_result('data')
后才能够结束。将 coro2()
作为第二个协程可能体现得不够明显,可以将协程的调用改成这样:
loop = asyncio.get_event_loop() loop.run_until_complete(asyncio.wait([ # coro1(), coro2(), coro1() ])) loop.close() 复制代码
输出的结果仍旧与上面相同。
其实, async
这个关键字的用法不止能用在函数上,还有 async with
异步上下文管理器, async for
异步迭代器. 对这些感兴趣且觉得有用的可以网上找找资料,这里限于篇幅就不过多展开了。
以上所述就是小编给大家介绍的《理解Python的协程(Coroutine)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 理解原型其实是理解原型链
- 要理解深度学习,必须突破常规视角去理解优化
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 荐 【C++100问】深入理解理解顶层const和底层const
- 深入理解 HTTPS
- 深入理解 HTTPS
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。