并发编程漫谈之 协程详解--以python协程入手(三)

前言:并发编程在当前的软硬件系统架构下,是一个程序员必备的知识技能。本文希望通过整理网上资料、结合自己的经验,提供一个系列分享,将基本的并发概念解释清楚。并希望在此基础上有所扩展,将各种语言的现状也所有对比。
一、并发编程漫谈之 基本概念
二、并发编程漫谈之 python多线程和多进程
三、并发编程漫谈之 协程详解–以python协程入手
四、并发编程漫谈之 C++中的各种锁
五、并发编程漫谈之 C++多进程和多线
六、并发编程漫谈之 C++协程的各种实现

一、为什么要引入协程

在现实应用中,耗时的IO操纵总是存在的,很多应用中还很频繁,比如网络操作。

在没有协程的时代,我们如何应对?

  • 同步编程:应用程序等待IO结果(比如等待打开一个大的文件,或者等待远端服务器的响应),阻塞当前线程;

    优点:符合常规思维,易于理解,逻辑简单;
    缺点:成本高昂,效率太低,其他与IO无关的业务也要等待IO的响应;

  • 异步多线程/进程:将IO操作频繁的逻辑、或者单纯的IO操作独立到一/多个线程中,业务线程与IO线程间靠通信/全局变量来共享数据;

    优点:充分利用CPU资源,防止阻塞资源
    缺点:线程切换代价相对较高,异步逻辑代码复杂(常见的处理方式是复杂的switch分支处理,或者复杂的状态机,或者复杂的DSL等)

  • 异步消息+回调:设计一个消息循环处理器,接收外部消息(包括系统通知和网络报文等),收到消息时调用注册的回调;

    优点:充分利用CPU资源,防止阻塞资源
    缺点:代码逻辑复杂

协程的概念,从一定程度来讲,可以说是“用同步的语义解决异步问题”,即业务逻辑看起来是同步的,但实际上并不阻塞当前线程(一般是靠事件循环处理来分发消息)。协程就是用来解决异步逻辑的编程复杂度问题的。

实际上,很多场景中协程也是作为 轻量级线程 的异步模型来使用的,比如Actor模型。这块在下面会讲。

二、协程的定义

在计算机科学中,例程(过程、函数、方法、子程序)被定义为操作的序列。例程的执行形成了父子关系,且孩子例程总是在父例程前结束。

举例说明,main函数中调用函数func1,func1中调用函数func2,此时就形成了父子关系(main为func1的父例程,func1是func2的父例程),func2执行结束进行函数栈回退,然后func1继续执行直到结束后进行函数栈回退,最后main继续执行直到结束后进行函数栈回退并退出程序。

协程的概念最早由Melvin Conway在1963年提出并实现,用于简化COBOL编译器的词法和句法分析器间的协作,当时他对协程的描述是“行为与主程序相似的子例程”。

Wiki的定义:协程是一种程序组件,是由子例程(过程、函数、例程、方法、子程序)的概念泛化而来的,子例程只有一个入口点且只返回一次,而协程允许多个入口点,可以在指定位置挂起和恢复执行。

协程通过保持执行状态,以能够明确的挂起和恢复,协程通过维护上下文提供了增强的控制流。

这是从直观上对协程的概念所做的理解,与1980年Marlin的论文中给出的定义类似,也是被广为引用的协程定义:

  • 协程的本地数据在后续调用中始终保持
  • 协程在控制离开时暂停执行,当控制再次进入时只能从离开的位置继续执行

三、协程原理 - 什么是协程

下面从Python中的协程演变,说明一下协程的原理。

1、从生成器generator和yield说起

假设现在有个需求,对某个已知规则的列表(比如 [1,2,3,4,。。。]),要求对列表里面的每个元素做某个操作,你怎么实现呢?

最容易想到的方式,可以如下实现:

def my_range(max_number)
	rst = []
	index = 0
	while index < max_number:
		rst.append(index)
		index += 1
	return rst

for i in my_range(max_number):
    do_something(i)
    print(i)

逻辑上没有任何问题。

不过,试想一下,如果max_number 是 100000000 呢?会占用多少内存?

再试想一下,如果对该列表的访问,会中途中断呢,比如满足某个条件即可提前终止,这又会浪费多少?

下面是一个改良版,可以做一下对比:

def lazy_range(max_number):
	index = 0
	while index < max_number:
		yield index
		index += 1

for i in lazy_range(max_number):
    do_something(i)
    print(i)

再假设有一个计算斐波那契序列的函数如下:

def old_fib(n):
    res = [0] * n
    index = 0
    a = 0
    b = 1
    while index <= n:
        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)

如果在遍历old_fib的过程中,遇到某个条件就退出了,那么old_fib中计算的很多结果都即浪费时间,又浪费内存。这时,yield就派上用场了。

def fib(n):
    index = 0
    a = 0
    b = 1
    while index <= n:
        yield b
        a, b = b, a + b
        index += 1


print('-'*10 + 'test yield fib' + '-'*10)
for fib_res in fib(20):
	do_something(fib_rst)
    print(fib_res)

当一个函数中包含yield语句时,python会自动将其识别为一个生成器。这时fib(20)并不会真正调用函数体,而是以函数体生成了一个生成器对象实例。

yield在这里可以保留fib函数的计算现场,暂停fib的计算并将b返回。而将fib放入for…in循环中时,每次循环都会调用next(fib(20)),唤醒生成器,执行到下一个yield语句处,直到抛出StopIteration异常。此异常会被for循环捕获,导致跳出循环。

2、Send

到这里可能还和协程没什么关系,但是实际上这已经是 Python 协程的雏形了,我们来看看维基上对于协程的定义:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations.

从某些角度来理解,协程其实就是一个可以暂停执行的函数,并且可以恢复继续执行。那么yield已经可以暂停执行了,如果在暂停后有办法把一些 value 发回到暂停执行的函数中,那么 就是了『协程』。于是在PEP 342中,添加了 “把东西发回已经暂停的生成器中” 的方法,这个方法就是send(),并且在 Python2.5 中得到了实现。这样就可以实现在各个生成器中 交互 了,也就是形成了 协程

于是,通过生成器的send函数,yield表达式也拥有了返回值。

利用这个特性我们继续改写range函数:

def smart_range(max_number):
	index = 0
	while index < max_number:
		jump = yield index
		if jump is None:
			jump = 1
		index += jump 

for i in smart_range(max_number):
    do_something(i)
    print(i)

# 可以控制步进,但显然麻烦一些
r = smart_range(10)
rst = next(r)
while True:
	try:
		print(rst)
		rst = r.send(2)
	except StopIteration as e:
		print('Generator return value:', e.value)
		break

我们用send这个特性,模拟一个额慢速斐波那契数列的计算:

def stupid_fib(n):
    index = 0
    a = 0
    b = 1
    while index <= n:
        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!

3、yield from

yield from用于重构生成器,可以像一个 管道一样 将send信息传递给内层协程, 并且处理好了各种异常情况。先看几个例子:

def first_gen():
    for c in "AB":
        yield c
    for i in range(0, 3):
        yield i

print(list(first_gen()))
 
def second_gen():
    yield from "AB"
    yield from range(0, 3)
 
print(list(second_gen()))

output:
['A', 'B', 0, 1, 2]
['A', 'B', 0, 1, 2]

在second_gen()中使用yield from subgen()时,second_gen()是陷入阻塞的,真正在交互的是调用second_gen()的那一方,也就是调用方和subgen()在交互,second_gen()的阻塞状态会一直持续到subgen()调用完毕,才会接着执行yield from后续的代码。

因此,对于stupid_fib也可以这样包装和使用:

def copy_stupid_fib(n):
    print('I am copy from stupid fib')
    yield from stupid_fib(n)
    print('Copy end')

print('-'*10 + 'test yield from and send' + '-'*10)
N = 20
csfib = copy_stupid_fib(N)
fib_res = next(csfib)
while True:
    print(fib_res)
    try:
        fib_res = csfib.send(random.uniform(0, 0.5))
    except StopIteration:
        break

如果没有yield from,这里的copy_yield_from将会特别复杂(因为要自己处理各种异常)。下面是python yield form 的实现示例,可以感受一下:

#一些说明
"""
_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象
"""

_i = iter(EXPR)

try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value

else:
    while 1:
        try:
            _s = yield _y
        except GeneratorExit as _e:
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        except BaseException as _e:
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:
                raise _e
            else:
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:
            try:
                if _s is None:
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:
                _r = _e.value
                break
RESULT = _r

4、asyncio.coroutine 和 yield from

yield from在 asyncio 模块中得以发扬光大。之前都是我们手工切换协程,现在当声明函数为协程后,我们通过事件循环来调度协程。先看示例代码来理解一下时间循环:

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()
loop.run_until_complete(print_sum(1, 2))
loop.close()

output:
Compute 1 + 2 ...
1 + 2 = 3

结合序列图来理解一下:
在这里插入图片描述

import asyncio,random
@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是一个基于事件循环的实现异步I/O的模块。通过yield from,我们可以将协程asyncio.sleep的控制权交给事件循环,然后挂起当前协程;之后,由事件循环决定何时唤醒asyncio.sleep,接着向后执行代码。

协程之间的调度都是由事件循环决定。

yield from asyncio.sleep(sleep_secs) 这里不能用time.sleep(1),因为time.sleep()返回的是None,它不是iterable,还记得前面说的yield from后面必须跟iterable对象(可以是生成器,迭代器)。

5、async和await

弄清楚了asyncio.coroutine和yield from之后,在Python3.5中引入的async和await就不难理解了:可以将他们理解成asyncio.coroutine/yield from的完美替身。当然,从Python设计的角度来说,async/await让协程表面上独立于生成器而存在,将细节都隐藏于asyncio模块之下,语法更清晰明了。

加入新的关键字 async ,可以将任何一个普通函数变成协程。上一节中的例子可以改写为:

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()
loop.run_until_complete(print_sum(1, 2))
loop.close()

output:
Compute 1 + 2 ...
1 + 2 = 3

以及:

import time,asyncio,random
async def smart_fib(n):
    index = 0
    a = 0
    b = 1
    while index < n:
        sleep_secs = random.uniform(0, 0.2)
        await asyncio.sleep(sleep_secs)
        print('Smart one think {} secs to get {}'.format(sleep_secs, b))
        a, b = b, a + b
        index += 1

async def stupid_fib(n):
    index = 0
    a = 0
    b = 1
    while index < n:
        sleep_secs = random.uniform(0, 0.4)
        await asyncio.sleep(sleep_secs)
        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()

示例程序中都是以sleep为异步I/O或者其他复杂任务的代表,在实际项目中, 可以使用协程异步的读写网络、读写文件、渲染界面等,而在等待协程完成的同时,CPU还可以进行其他的计算。协程的作用正在于此。

6、一个实例

import asyncio
import time
import aiohttp
import async_timeout

msg = "http://www.nationalgeographic.com.cn/photography/photo_of_the_day/{}.html"
urls = [msg.format(i) for i in range(4500, 5057)]

async def fetch(session, url):
    with async_timeout.timeout(10): # 下面的流程如果10s还搞不定,就超时
        async with session.get(url) as response:
            return response.status

async def main(url):
    async with aiohttp.ClientSession() as session:
            status = await fetch(session, url)
            return status

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [main(url) for url in urls]

    status_list = loop.run_until_complete(asyncio.gather(*tasks))
    print(len([status for status in status_list if status==200]))

四、什么是事件循环

上面例子中,一直没细讲事件循环(event_loop)的作用。事件循环很重要,也很常见。

做过GUI编程或网络前端编程的,可能更容易理解event_loop,这两种场景的基础就是event_loop。

Wikipedia 上定义:

an event loop “is a programming construct that waits for and dispatches events or messages in a program”.

简单来说,event loop 就完成一件事:当A发生的时候,去做B这件事。比如界面GUI程序,当你点击某个按钮的时候,就触发某个动作。当然前提是,提前要注册 onclick 方法,即 click 事件的回调(callback)。

event loop 除了完成 事件 <----> 触发器 的映射和触发动作外,更多的时候就做为一个调度器,完成整个程序的调度工作。

以上面python协程的例子,python标准库asyncio中就自带了事件循环,并且承担了调度器的工作。event_loop 搜集所有的事件(包括定时、网络、外部IO及其他事件),找到对应的协程,将消息投递过去,让对应的协程处理。

下面是一个简单的例子:

import asyncio

async def countdown2(label, length, delay=0):
    print(label, 'waiting', delay, 'seconds before starting countdown')
    delta = await asyncio.sleep(delay)
    print(label, 'starting after waiting', delta)
    while length:
        print(label, 'T-minus', length)
        waited = await asyncio.sleep(1)
        length -= 1
    print(label, 'lift-off!')

loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(countdown2("A", 5)),
    asyncio.ensure_future(countdown2("B", 3, delay=2)),
    asyncio.ensure_future(countdown2("C", 4, delay=1))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

output:
A waiting 0 seconds before starting countdown
B waiting 2 seconds before starting countdown
C waiting 1 seconds before starting countdown
A starting after waiting None
A T-minus 5
C starting after waiting None
C T-minus 4
A T-minus 4
B starting after waiting None
B T-minus 3
C T-minus 3
A T-minus 3
B T-minus 2
C T-minus 2
A T-minus 2
B T-minus 1
C T-minus 1
A T-minus 1
B lift-off!
C lift-off!
A lift-off!

现在我们把上面例子中的事件循环自己实现一下:

import datetime
import heapq
import types
import time

class Task:
    def __init__(self, wait_until, coro):
        self.coro = coro
        self.waiting_until = wait_until

    def __eq__(self, other):
        return self.waiting_until == other.waiting_until

    def __lt__(self, other):
        return self.waiting_until < other.waiting_until

class SleepingLoop:
    def __init__(self, *coros):
        self._new = coros
        self._waiting = []

    def run_until_complete(self):
        # Start all the coroutines.
        for coro in self._new:
            wait_for = coro.send(None)
            heapq.heappush(self._waiting, Task(wait_for, coro))
        # Keep running until there is no more work to do.
        while self._waiting:
            now = datetime.datetime.now()
            # Get the coroutine with the soonest resumption time.
            task = heapq.heappop(self._waiting)
            if now < task.waiting_until:
                # We're ahead of schedule; wait until it's time to resume.
                delta = task.waiting_until - now
                time.sleep(delta.total_seconds())
                now = datetime.datetime.now()
            try:
                # It's time to resume the coroutine.
                wait_until = task.coro.send(now)
                heapq.heappush(self._waiting, Task(wait_until, task.coro))
            except StopIteration:
                # The coroutine is done.
                pass

@types.coroutine
def sleep(seconds):
    now = datetime.datetime.now()
    wait_until = now + datetime.timedelta(seconds=seconds)
    # Make all coroutines on the call stack pause; the need to use `yield`
    # necessitates this be generator-based and not an async-based coroutine.
    actual = yield wait_until
    # Resume the execution stack, sending back how long we actually waited.
    return actual - now

async def countdown(label, length, *, delay=0):
    """Countdown a launch for `length` seconds, waiting `delay` seconds.

    This is what a user would typically write.
    """
    print(label, 'waiting', delay, 'seconds before starting countdown')
    delta = await sleep(delay)
    print(label, 'starting after waiting', delta)
    while length:
        print(label, 'T-minus', length)
        waited = await sleep(1)
        length -= 1
    print(label, 'lift-off!')

def main():
    """Start the event loop, counting down 3 separate launches.

    This is what a user would typically write.
    """
    loop = SleepingLoop(countdown('A', 5), countdown('B', 3, delay=2),
                        countdown('C', 4, delay=1))
    start = datetime.datetime.now()
    loop.run_until_complete()
    print('Total elapsed time is', datetime.datetime.now() - start)

if __name__ == '__main__':
    main()

有兴趣的可以研究一下JavaScript的event_loop 或者 QT 的事件循环,也可以对比一下 epoll (另参考)的特点。

五、协程的优点

从上面协程的原理,可以看出协程的优点:

  • 开销小:这是协程能被应用的基石,没有这点,其他优势都不存在。
  • 好控制:进程、线程都是操作系统调度的,虽然线程在一定程度上也可以被应用程序控制状态,但是程度有限,复杂度高。而协程的调度完全由应用程序自己控制。这点是 并发的基础
  • 易建模:因为协程的调度,是应用程序自己控制的,基于此,可以做很多定制性很高的建模,让程序按照程序员希望的顺序执行。

基于上述开销小、好控制的优点,可以用协程应对很多实际问题,简化建模的复杂度。

六、协程的分类

来看Wiki举出的,也是解释协程时最常见的生产-消费者模型的例子:

var q := new queue
 
coroutine produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield to consume

coroutine consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield to produce

下面是基于生成器的生产-消费者模型实现(依然来自Wiki):

var q := new queue

generator produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield consume

generator consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield produce

subroutine dispatcher
    var d := new dictionary (generator → iterator)
    d[produce] := start produce
    d[consume] := start consume
    var current := produce
    loop
        current := next d[current] 

根据上文协程的定义,生成器版本的代码完全符合协程的概念,生成器本身应该就是协程。

两种明显不同的控制结构,却都符合协程的定义,问题出在哪里?

之前的协程定义的问题在于不够精确,遗留下了开放的,关于协程结构的问题。这导致了协程概念的模糊,造成理解上的困扰。这个问题也部分导致了主流语言一直缺乏对协程的支持。甚至在描述一些本质上属于协程的机制时,如Windows的纤程(Fiber),连协程这个术语都很少被提起。

2004年Lua的作者Ana Lucia de Moura和Roberto Ierusalimschy发表的论文Revisiting Coroutines中,对协程进行了分类,论文中依照三个问题区分协程:

  • 控制传递(Control-transfer)机制
  • 协程是否为栈式(Stackful)构造
  • 协程是否作为语言的第一类(First-class)对象提供

1、对称与非对称协程

控制传递机制的不同区分出了对称(Symmetric)和非对称(Asymmetric)协程。

  • 对称协程:
    • 只提供一种传递操作,用于在协程间直接传递控制。
    • 对称协程都是等价的,控制权直接在对称协程之间进行传递,即对称协程在挂起时主动指明另外一个对称协程来接收控制权。
  • 非对称协程(半对称(Semi-symmetric)协程或半(Semi)协程):
    • 提供调用和挂起两种操作,挂起时控制返回给调用者。
    • 非对称协程知道它的调用者,其在挂起时转让控制权给它的调用者,然后调用者根据算法调用其他非对称协程进行工作。

在我们的生产-消费者模型的例子中,前者是对称协程,生成器是一种非对称协程。

出于支持并发而提供的协程通常是对称协程,用于表示独立的执行单元,如golang中的协程。用于产生值序列的协程则为非对称协程,如迭代器和生成器。

在很长一段时间里的普遍看法是,对称与非对称协程的能力不同。所以一些支持通用协程机制的语言同时提供了这两类控制传递,如Simula和BCPL。

事实上很容易证明这两种控制传递机制可以相互表达,因此要提供通用协程时只须实现其中一种即可。但是,两者表达力相同并不意味着在易用性上也相同。对称协程会把程序的控制流变得相对复杂而难以理解和管理,而非对称协程的行为在某种意义上与函数类似,因为控制总是返回给调用者。使用非对称协程写出的程序更加结构化。

2、第一类(First-class)与受限协程

协程被作为first-class对象提供,那么其可以作为参数被传递,由函数创建并返回,并存储在一个数据结构中供后续操作,也可以被开发者自由的维护。从上面的描述可以看到,first-class对象主要为协程提供了良好的表达力,方便开发者对协程进行操作。

协程是否作为语言的第一类对象提供对表达力的影响极大。为特定用途而实现的协程,往往把协程对象限制在 指定的代码结构中,无法由程序员直接控制。一些语言实现的迭代器(CLU,Sather)和生成器(Icon)被限制在某个循环内使用,属于受限协程。只有实现为第一类对象的协程可以提供自定义控制结构的能力,而这种能力正是协程强大的表现力所在。比如golang、erlang、scala 等。

3、栈式(Stackful)构造

  • 栈式协程(Stackful):为每个协程分配独立的上下文空间以保存栈数据。允许在内部的嵌套函数中挂起,恢复时从挂起点继续执行。
  • 非栈式协程(Stackless):没有独立的上下文空间,数据只能保存在堆上。所以只能在主体部分执行挂起操作,可以用来开发简单的迭代器或生成器,但遇到复杂些的控制结构时,会把问题搞得更加复杂。

例如,如果生成器的生成项是通过递归或辅助函数生成的,必须创建出一系列相应层级结构的辅助生成器连续生成项直到到达原始调用点。非栈式协程也不足以实现用户级多任务。

七、常见基于协程的模型

下面介绍的模型,以及示例,并不是最合适的,各个模型没有抽象层次高下之分,只是对目前一些应用场景的整理。还有很多应用方式需要慢慢整理进来。

很多语言只提供了协程的底层语法,由应用者自己发挥和封装;有的语言则提供了基于协程的高级语法,对某些场景的应用代理非常大的简化。

1、生产者-消费者

在这里插入图片描述

一个简单的python版本示例,下面的代码显然是不能用于实际应用的:

import asyncio

def customer():
	a = 0
	while True:
		a = yield a
		print("a = %d" % a)

async def producer(c):
	for i in range(5):
		b = c.send(i)
		print("producer b = %d" % b)
		await asyncio.sleep(0.2)

async def producer2(c):
	for i in range(5):
		b = c.send(i)
		print("producer2 b = %d" % b)
		await asyncio.sleep(0.3)

if __name__ == '__main__':
	loop = asyncio.get_event_loop()
	c = customer()
	c.send(None)
	tasks = [
		producer(c),
		producer2(c),
	]
	loop.run_until_complete(asyncio.wait(tasks))
	print('All fib finished.')
	loop.close()

output:
a = 0
producer b = 0
a = 0
producer2 b = 0
a = 1
producer b = 1
a = 1
producer2 b = 1
a = 2
producer b = 2
a = 2
producer2 b = 2
a = 3
producer b = 3
a = 4
producer b = 4
a = 3
producer2 b = 3
a = 4
producer2 b = 4

2、状态机

状态迁移如图:
在这里插入图片描述
示例代码:

import asyncio
import datetime
import time
from random import randint

async def StartState():
    print("Start State called \n")
    input_value = randint(0, 1)
    time.sleep(1)
    if (input_value == 0):
        result = await State2(input_value)
    else:
        result = await State1(input_value)
    print("Resume of the Transition: \nStart State calling " + result)

async def State1(transition_value):
    outputValue = "State 1 with transition value = %s \n" %(transition_value)
    input_value = randint(0, 1)
    time.sleep(1)
    print("...Evaluating State1....")
    if (input_value == 0):
        result = await State3(input_value)
    else:
        result = await State2(input_value)

    result = "State 1 calling " + result
    return (outputValue + str(result))

async  def State2(transition_value):
    outputValue = "State 2 with transition value = %s \n" %(transition_value)
    input_value = randint(0, 1)
    time.sleep(1)
    print("...Evaluating State2....")
    if (input_value == 0):
        result = await State1(input_value)
    else:
        result = await State3(input_value)

    result = "State 2 calling " + result
    return (outputValue + str(result))

async def State3(transition_value):
    outputValue = "State 3 with transition value = %s \n" %(transition_value)
    input_value = randint(0, 1)
    time.sleep(1)
    print("...Evaluating State3....")
    if (input_value == 0):
        result = await State1(input_value)
    else:
        result = await EndState(input_value)

    result = "State 3 calling " + result
    return (outputValue + str(result))

async def EndState(transition_value):
    outputValue = "End State with transition value = %s \n" %(transition_value)
    print("...Stop Computation...")
    return (outputValue)

if __name__ == "__main__":
    print("Finite State Machine simulation With Asyncio Coroutine")
    loop = asyncio.get_event_loop()
    loop.run_until_complete(StartState())

output:
Finite State Machine simulation With Asyncio Coroutine
Start State called 

...Evaluating State1....
...Evaluating State3....
...Evaluating State1....
...Evaluating State2....
...Evaluating State3....
...Stop Computation...
Resume of the Transition: 
Start State calling State 1 with transition value = 1 
State 1 calling State 3 with transition value = 0 
State 3 calling State 1 with transition value = 0 
State 1 calling State 2 with transition value = 1 
State 2 calling State 3 with transition value = 1 
State 3 calling End State with transition value = 1 

3、Actor模型

Actor的概念来自于Erlang,在AKKA中(scala),可以认为一个Actor就是一个容器,用以存储状态、行为、Mailbox以及子Actor与Supervisor策略。Actor之间并不直接通信,而是通过Mail来互通有无。

每个Actor都有一个(恰好一个)Mailbox。Mailbox相当于是一个小型的队列,一旦Sender发送消息,就是将该消息入队到Mailbox中。入队的顺序按照消息发送的时间顺序。

Mailbox有多种实现,默认为FIFO。但也可以根据优先级考虑出队顺序,实现算法则不相同。
在这里插入图片描述

这样的设计解耦了actor之间的关系——actor都以自己的步调运行,且发送消息时不会被阻塞。虽然所有actor可以同时运行,但它们都按照信箱接收消息的顺序来依次处理消息,且仅在当前消息处理完成后才会处理下一个消息,因此我们只需要关心发送消息时的并发问题即可。

erlang 和 scala 是典型的 actor 模型。

4、CSP并发模型

CSP(communicating sequential processes)并发模型是在1970年左右提出的概念,属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。

请记住下面这句话:

Do not communicate by sharing memory; instead, share memory by communicating.
“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”

golang是典型的CSP模型。

在CSP模型中,协程与协程间不直接通信,也不像Actor模型那样直接向目标协程投递信息,而是通过一个Channel来交换数据。

在这里插入图片描述

其实所有的并发模型,都是由两个要素组成:传递的数据 + 处理数据的对象

actor 和 CSP 也符合上述特征,不过还是有些区别:

  • 关于消息发送方和接收方

    • Actor:注重的处理单元,也就是Actor,而不是消息传送方式。发送消息时,都需要知道对方是谁。

      这里的“都需要知道对方是谁”的意思,当ActorX要给ActorY发消息时,必须明确知道ActorY的地址。ActorY接收到消息时,就能够知道消息发送者(ActorX)的地址。返回消息给发送者时,只需要按发送者的地址往回传消息就行。

    • CSP:注重的是消息传送方式(channel),不关心发送的人和接收的人是谁。

      向channel写消息的人,不知道消息的接收者是谁;读消息的人,也不知道消息的写入者是谁。

  • 消息传输方式

    • Actor:每一对Actor之间,都有一个“MailBox”来进行收发消息。消息的收发是异步的。
    • CSP:使用定义的 channel 进行收发消息。消息的收发是同步的(也可以做成异步的,但是一个有限异步)
  • 消息处理对象与通信方式的关系

    • Actor中,每个Actor对消息接收和发送的支持是天然的。
    • CSP中,一个对象(协程)可以没有通信方式(没有channel),二者是解耦的。
  • 应用场景侧重不同

    • Actor侧重对现实对象的抽象,是消息处理对象的实例化。
    • CSP侧重于协作分工,更像Boss-Worker模式的任务分发机制。

八、参考:

1、https://www.cnblogs.com/jiayy/p/3241115.html
2、http://www.sohu.com/a/237171690_465221
3、https://blog.csdn.net/soonfly/article/details/78361819
4、http://www.woola.net/detail/2016-10-18-python-coprocessor.html
5、https://blog.csdn.net/soonfly/article/details/78361819
6、https://blog.csdn.net/qq_41841569/article/details/80324334
7、https://www.cnblogs.com/aguncn/p/6124928.html
8、http://www.python88.com/topic/12579
9、https://blog.csdn.net/hyman_yx/article/details/52251261
10、https://blog.csdn.net/wuhenyouyuyouyu/article/details/52709395
11、https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值