进程、线程与协程
首先明白并发与并行的区别:
- 并发指的是在某一时间段,有多个程序在同一个 CPU 上运行,在任意一个时间点,只有一个在运行;
- 并行指的是多个 CPU 同时处理多个任务,强调的是任务处理的同时性。
举个直观的例子:
- 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行;
- 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发;
- 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
进程
是资源分配的最小单位。 同一时刻执行的进程数不会超过核心数(因为一个进程至少含有一个线程)。
线程
是CPU 调度的最小单位。
设置的线程的原因是一个程序需要有很多个任务进行协同工作。举个例子:
用播放器看视频时,视频输出的画面和输出的声音可以认为是两种任务。当拖动进度条时又触发了另一种任务。拖动进度条会导致画面和声音都实时发生变化。如果没有线程的话,由于单一程序是阻塞的,那么可能发生的情况就是:拖动进度条→画面更新→声音更新。你会明显感到画面和声音和进度条不同步。
线程的调度与切换比进程快很多。
协程
协程是更加轻量级的线程, 又称微线程,纤程。英文名 Coroutine。一个线程可以包含一个或者多个协程。
协程的一个重要特点是在用户态执行,操作系统并不能感知到协程的存在。
协程的一个重要优点是不需要进行线程切换导致的上下文切换,效率更高。
一个线程所包含的所有协程是不可能同时执行的,它们之间是同步的,这点和多个线程的执行有所区别。
举一个 Python 中协程的例子:
Python 的 yield 不但可以返回一个值,它还可以接收调用者发出的参数。
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
执行结果为:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[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 协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
最后套用 Donald Knuth 的一句话总结协程的特点:
子程序就是协程的一种特例。
Python yield/send, yield from, async/await
yield 生成器:
def mygen(alist):
while len(alist) > 0:
c = randint(0, len(alist)-1)
yield alist.pop(c)
a = ["aa","bb","cc"]
c=mygen(a)
print(c)
输出:<generator object mygen at 0x02E5BF00>
如果我们要迭代这个生成器,那么结果会一个一个 yield 出来,这把计算 delay 到了需要计算的时候。
yield/send:
ret = g.send(num) 是先返回 ret 也就是先 yield,然后发送 num 进去也就是 receive,注意这个顺序。
def gen():
value = 0
while True:
receive = yield value
value = 'got: %s' % receive
g = gen()
print(g.send(None))
print(g.send('hello'))
print(g.send(123456))
print(g.send('e'))
输出结果:
0
got: hello
got: 123456
got: e
这么来看,生成器算是一个对于 yield/send 体系的特化,yield/send 体系中,不仅仅协程要往外 yield 返回值,外面也需要往里面 send 值,生成器体系省去了 send 值这部分,仅仅让生成器一直不断往外 yield 值。
yield from:
def g1():
yield range(5)
def g2():
yield from range(5)
it1 = g1()
it2 = g2()
for x in it1:
print(x)
for x in it2:
print(x)
输出:
range(0, 5)
0
1
2
3
4
这说明 yield 就是将 range 这个可迭代对象直接返回了。 而 yield from 解析了 range 对象,将其中每一个 item 返回了。 yield from iterable 本质上等于 for item in iterable: yield item 的缩写版。算是一个小 tick,一个语法糖。
yield from 在 asyncio 模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。
import asyncio,random
# 注意,asyncio.coroutine 已经 deprecated 了!!!,这里仅作演示,不保证能够运行。
@asyncio.coroutine
def smart_fib(n):
index = 0
a = 0
b = 1
while index < n:
sleep_secs = random.uniform(0, 0.2)
yield from asyncio.sleep(sleep_secs) # 通常 yield from 后都是接的耗时操作
print('Smart one think {} secs to get {}'.format(sleep_secs, b))
a, b = b, a + b
index += 1
@asyncio.coroutine
def stupid_fib(n):
index = 0
a = 0
b = 1
while index < n:
sleep_secs = random.uniform(0, 0.4)
yield from asyncio.sleep(sleep_secs) #通常 yield from 后都是接的耗时操作
print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
a, b = b, a + b
index += 1
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [
smart_fib(10),
stupid_fib(10),
]
loop.run_until_complete(asyncio.wait(tasks))
print('All fib finished.')
loop.close()
yield from 语法可以让我们方便地调用另一个 generator。本例中 yield from 后面接的 asyncio.sleep() 是一个 coroutine(里面也用了 yield from),所以线程不会等待 asyncio.sleep(),而是直接中断并执行下一个消息循环。当 asyncio.sleep() 返回时,线程就可以从 yield from 拿到返回值(此处是 None),然后接着执行下一行语句。所以我们能够看出来,asyncio.sleep() 的好处就是其实并不是真的 sleep,而是在 sleep 这段时间让给其他协程来执行。
asyncio 是一个基于事件循环的实现异步 I/O 的模块。通过 yield from,我们可以将协程 asyncio.sleep 的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒 asyncio.sleep(看什么时候 asyncio 会 send 进来),接着向后执行代码。因此,协程之间的调度都是由事件循环决定。
async/await:
可以将他们理解成 asyncio.coroutine/yield from 的完美替身。在 Python 3.5 中引入,简单来说就是语法糖。只需要把 asyncio.coroutine/yield from 出现的地方替换成 async/await 就可以了。
await 除了可以 yield from 生成器,还有一大好处是可以直接接 async 修饰的函数。因为 async 函数本身就会返回一个生成器,await 就会每次都返回这个生成器里的一个 item 然后将控制权交回。
async 无法将一个生成器转换成协程。
import asyncio
async def async_task(name, duration):
print(f"Task {name} started. It will take {duration} seconds.")
await asyncio.sleep(duration) # 模拟一个 I/O 操作
print(f"Task {name} completed.")
async def main():
task1 = asyncio.create_task(async_task("A", 2))
task2 = asyncio.create_task(async_task("B", 4))
task3 = asyncio.create_task(async_task("C", 3))
await asyncio.gather(task1, task2, task3)
print("All tasks completed.")
if __name__ == "__main__":
asyncio.run(main())
大部分取自这篇文章,写得很好:理解Python协程:从yield/send到yield from再到async/await_python async send-CSDN博客
Python async with and async for
with ... as ...:
# 改成了
async with ... as ...:
# 同理
for ... in ...:
# 改成了
async for ... in ...:
异步上下文管理器指的是在 enter 和 exit 方法处能够暂停执行 yield 的上下文管理器。一个好处是比如说我们要在 enter 处建立连接,使用 with … as 需要在 enter 处等待建立连接完成的时候才能继续下一步,比如 post, get 等等。使用 async with … as 可以在 enter 处先发送建立连接的请求,然后 yield 出去,等待成功后再切回来就行了,同理我们也可以在 exit 处关闭一个连接,而不需要等待。必须要在 async def 函数内使用。
async for 是每次迭代都会 await 一次。必须要在 async def 函数内使用。这应该是为了防止 for 循环这个阻塞时间太长。