首先明白并发与并行的区别:

  1. 并发指的是在某一时间段,有多个程序在同一个 CPU 上运行,在任意一个时间点,只有一个在运行;
  2. 并行指的是多个 CPU 同时处理多个任务,强调的是任务处理的同时性。

举个直观的例子:

  1. 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行;
  2. 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发;
  3. 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。

进程

资源分配的最小单位。 同一时刻执行的进程数不会超过核心数(因为一个进程至少含有一个线程)。

线程

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 后:

  1. 首先调用 c.send(None) 启动生成器;
  2. 然后,一旦生产了东西,通过 c.send(n) 切换到 consumer 执行;
  3. consumer 通过 yield 拿到消息,处理,又通过 yield 把结果传回;
  4. produce 拿到 consumer 处理的结果,继续生产下一条消息;
  5. produce 决定不生产了,通过 c.close() 关闭 consumer,整个过程结束。

整个流程无锁,由一个线程执行,produceconsumer 协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用 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() 函数后,需要等待子程序返回才能继续进行。

Reference