进程、线程与协程
首先明白并发与并行的区别:
- 并发指的是在某一时间段,有多个程序在同一个 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
:
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
就可以了。
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博客
goroutine
Go 语言中的协程。使用 go 关键字 go func()
即可启动一个协程,并且它是处于异步方式运行,你不需要等它运行完成以后再执行以后的代码。这点和 python 的协程不一样,如上所述,python 在主程序执行了 send()
函数后,需要等待子程序返回才能继续进行。