Fork me on GitHub

python中的协程(一)

协程

协程概念及目的

1、协程:

单线程实现并发、在应用程序里控制多个任务的切换+保存状态

优点:

应用程序级别速度要远远高于操作系统的切换

缺点:

多个任务一旦有一个阻塞没有切,整个线程都阻塞在原地,该线程内的其他的任务都不能执行了

一旦引入协程,就需要检测单线程下所有的IO行为, 实现遇到IO就切换,少一个都不行,以为一旦一个任务阻塞了,整个线程就阻塞了, 其他的任务即便是可以计算,但是也无法运行了

2、协程序的目的:

想要在单线程下实现并发,主要用于io密集型

并发指的是多个任务看起来是同时运行的

并发=切换+保存状态

3、协程相比于线程:

最大的区别在于,协程不需要像线程那样来回的中断切换,也不需要线程的锁机制,因为线程中断或者锁机制都会对性能问题造成影响,所以协程的性能相比于线程,性能有明显的提高,尤其在线程越多的时候,优势越明显。


复习生成器

生成器的一个作用是类似于迭代器,每次迭代的值为yield右值(重点),还有一种是利用yield断点,然后切换到另一个任务,下面是第一种用法的复习

def f(maxx):
    n, a, b = 0, 1, 1
    while n < maxx:
        # print(b)
        y = yield b
        a, b = b, a + b
        n += 1
        print(y)
    return 'error_name'  # 原函数的return变成了迭代完报出错误的值(value)


fi = f(6)  # 将一个函数变成生成器,并赋值给fi,每次迭代的值都是yield右边的值
print(fi.__next__())  # 运行一次生成器,到yield处中断,运行下面的程序
print(fi.send('Done'))  # 回到第一次运行的生成器的yield中断处,并把Done赋予yield,然后执行下面的程序,到yield再次停止
fi.send('Done')  # 如果这句换成print(fi.send('Done')),则会输出3
print(fi.send('Done'))  # next和send的返回值都是fi迭代器本次的值

########################
1
Done
2
Done
Done
5

这里有一点要注意,要激活一个生成器,一定要调用next()方法或者send(None)启动生成器,而不是调用生成器的send()方法,如果直接调用send()方法会报错,同时在生成器这里,最先调用 next() 函数这一步通常称为“预激”(prime)协程(即,让协程向前执行到第一个 yield 表达式,准备好作为活跃的协程使用)。

从Python2.5 开始,我们可以在生成器上调用两个方法,显式的把异常发给协程。 这两个方法是throw和close

generator.throw详解

generator.throw(exc_type[, exc_value[, traceback]])

外层代码调用throw使生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前(循环)执行到下一个yield表达式,而产出的值会成为调用throw方法得到的返回值。如果没有处理,则向上冒泡(传递给外层,值传递给外一层)。

def myGenerator():
    value = 1
    while True:
        yield value
        value += 1
 
 
gen = myGenerator()
print gen.next()
print gen.next()
print gen.throw(Exception, "Method throw called!")

  输出结果为

1
2
Traceback (most recent call last):
  File "test.txt", line 11, in <module>
    print gen.throw(Exception, "Method throw called!")
  File "test.txt", line 4, in myGenerator
    yield value
Exception: Method throw called!

  外层代码的最后一句向生成器对象抛出了一个异常。但是,在生成器对象的方法时没有处理该异常的代码,因此异常会被抛出到主方法,主方法任然未处理,最终报错。

下面的示例中,添加了处理异常的代码

def myGenerator():
    value = 1
    while True:
        try:
            yield value
            value += 1
        except:
            value = 1
 
 
gen = myGenerator()
print gen.next()
print gen.next()
print gen.throw(Exception, "Method throw called!")

  代码的输出如下

1
2
1
Exception RuntimeError: 'generator ignored GeneratorExit' in <generator object myGenerator at 0x00000000028BB900> ignored
  上面输出中,第2个1是gen.throw方法的返回值。在执行完该方法后,生成器对象方法的while循环并没有结束,也即是说生成器方法的执行还没有结束。这个时候如果强制结束主程序,会抛出一个RuntimeError(这里不懂下面在介绍)。也就是上面输出的第4行。要优雅地关闭主程序,需要用到生成器对象的close方法。下面继续介绍。

GeneratorExit异常

当一个生成器对象被销毁时,或者生成器遇到异常退出时,会抛出一个GeneratorExit异常。请看下面的代码。
def myGenerator():  
    try:
        yield 1
    except GeneratorExit:
        print "myGenerator exited"
 
gen = myGenerator()
print gen.next()

输出结果为
1
myGenerator exited

  上面代码的运行逻辑如下: 当调用到gen.next()方法时,会执行生成器对象方法的yield语句。此后,主程序结束,系统会自动产生一个GeneratorExit异常,被生成器对象方法的Except语句块截获。

  值得一提的是,GeneratorExit异常只有在生成器对象被激活后,才有可能产生。更确切的说,需要至少调用一次生成器对象的next方法后,系统才会产生GeneratorExit异常。请看下面的代码。

def myGenerator():  
    try:
        yield 1
        yield 2
    except GeneratorExit:
        print "myGenerator exited"
 
gen = myGenerator()
del gen
print "Main caller exited"

  其输出结果如下:

Main caller exited

  在上面的示例中,我们都显式地捕获了GeneratorExit异常。如果该异常没有被显式捕获,生成器对象也不会把该异常向主程序抛出。因为GeneratorExit异常定义的初衷,是方便开发者在生成器对象调用结束后定义一些收尾的工作,如释放资源等。

generator.close()

  生成器对象的close方法会在生成器对象方法的挂起处抛出一个GeneratorExit异常。GeneratorExit异常产生后,系统会继续把生成器对象方法后续的代码执行完毕。参见下面的代码。

def myGenerator():  
    try:
        yield 1
        print "Statement after yield"
    except GeneratorExit:
        print "Generator error caught"
 
    print "End of myGenerator"
 
gen = myGenerator()
print gen.next()
gen.close()
print "End of main caller"

代码执行过程如下:

  • 当调用gen.next方法时,会激活生成器,直至遇到生成器方法的yield语句,返回值1。同时,生成器方法的执行被挂起。
  • 当调用gen,close方法时,恢复生成器方法的执行过程。系统在yield语句处抛出GeneratorExit异常,执行过程跳到except语句块。当except语句块处理完毕后,系统会继续往下执行,直至生成器方法执行结束。

代码的输出如下:

1
Generator error caught
End of myGenerator
End of main caller

  需要注意的是,GeneratorExit异常的产生意味着生成器对象的生命周期已经结束。因此,一旦产生了GeneratorExit异常,生成器方法后续执行的语句中,不能再有yield语句,否则会产生RuntimeError。请看下面的例子。

def myGenerator():  
    try:
        yield 1
        print "Statement after yield"
    except GeneratorExit:
        print "Generator error caught"
 
    yield 3
 
gen = myGenerator()
print gen.next()
gen.close()
print "End of main caller"


输出结果为
1
Generator error caught
Traceback (most recent call last):
  File "test.txt", line 12, in <module>
    gen.close()
RuntimeError: generator ignored GeneratorExit

  注意,由于RuntimError会向主方法抛出,因此主方法最后的print语句没有执行。

  有了上面的知识,我们就可以理解为什么下面的代码会抛出RuntimError错误了。

def myGenerator():  
    value = 1  
    while True:  
        try:  
            yield value  
            value += 1  
        except:  
            value = 1  
  
  
gen = myGenerator()  
print gen.next()  
print gen.next()  
print gen.throw(Exception, "Method throw called!")  

  上面代码中,当主程序结束前,系统产生GeneratorExit异常,被生成器对象方法的except语句捕获,但是此时while语句还没有退出,因此后面还会执行“yield value”这一语句,从而发生RuntimeError。要避免这个错误非常简单,请看下面的代码。

def myGenerator():  
    value = 1  
    while True:  
        try:  
            yield value  
            value += 1  
        except Exception:  
            value = 1  
  
  
gen = myGenerator()  
print gen.next()  
print gen.next()  
print gen.throw(Exception, "Method throw called!")  

  代码第7行的except语句声明只捕获Exception异常对象。这样,当系统产生GeneratorExit异常后,不再被except语句捕获,继续向外抛出,从而跳出了生成器对象方法的while语句。

  这里再简单说一句,GeneratorExit异常继承自BaseException类。BaseException类与Exception类不同。一般情况下,BaseException类是所有内建异常类的基类,而Exception类是所有用户定义的异常类的基类。

生成器和协程

生成器和协程都是通过python中的yield的关键字实现的,不同的是,生成器的任务指向不可控,协程可以理解为任务指向可控的生成器

写成是:程序员可控制的并发流程,不管是进程还是线程,其切换都是操作系统在调度,而对于协程,程序员可以控制什么时候切换出去,什么时候切换回来

生成器可以作为协程使用。协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。

尽管生成器和协程看起来很像,但是它们代表的却是完全不同的设计理念。生成器是用来生成数据的,而协程从某种意义上来说是消耗数据的。协程和迭代无关,尽管协程也会用next来获取数据,但是协程和迭代无关,不要尝试像使用生成器那样去迭代地使用协程。


协程(生成器)有四种状态,分别是

GEN_CREATED:等待执行

GEN_RUNNING:解释器执行

GEN_SUSPENDED:在yield表达式处暂停

GEN_CLOSED:执行结束

协程(生成器)的状态可以用inspect.getgeneratorstate()函数来确定,这里是:inspect模块官方文档

中文方面参考如下:https://blog.csdn.net/qq_26398495/article/details/80109689

来看下面的例子:

from inspect import getgeneratorstate
from time import sleep
import threading


def get_state(coro):
    print("其他线程生成器状态:%s", getgeneratorstate(coro))  # <1>


def simple_coroutine():
    for i in range(3):
        sleep(0.5)
        x = yield i + 1  # <1>


my_coro = simple_coroutine()
print("生成器初始状态:%s" % getgeneratorstate(my_coro))  # <2>
first = next(my_coro)
for i in range(5):
    try:
        my_coro.send(i)
        print("主线程生成器初始状态:%s" % getgeneratorstate(my_coro))  # <3>
        t = threading.Thread(target=get_state, args=(my_coro,))
        t.start()
    except StopIteration:
        print("生成器的值拉取完毕")
print("生成器最后状态:%s" % getgeneratorstate(my_coro))  # <4>

#################
生成器初始状态:GEN_CREATED
主线程生成器初始状态:GEN_SUSPENDED
其他线程生成器状态:%s GEN_SUSPENDED
主线程生成器初始状态:GEN_SUSPENDED
其他线程生成器状态:%s GEN_SUSPENDED
生成器的值拉取完毕
生成器的值拉取完毕
生成器的值拉取完毕
生成器最后状态:GEN_CLOSED

  装饰器加yield实现协程,生成器可实现自动循环,加上异常捕捉,生成器中的return为异常stopitertions的值

from functools import wraps
 
 
def coroutine(func):
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
 
    return primer
 
 
@coroutine
def averager():
    total = .0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total / count
 
 
try:
    coro_avg = averager()  # 预激装饰器
    print(coro_avg.send(10))
    print(coro_avg.send(20))
    print(coro_avg.send(30))
    coro_avg.close()
    print(coro_avg.send(40))
except StopIteration:
    print("协程已结束")
except TypeError:
        print("传入值异常")

####################
10.0
15.0
20.0
协程已结束

协程(生成器)的返回值

之前我们知道,生成器的返回值是异常StopIteration的值,具体获取值的方式可以如此

from collections import namedtuple

Result = namedtuple('Result', 'count average')


def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield  # yield右边是每次迭代的值,不写则为None
        if term is None:
            break  # 为了返回值,协程必须正常终止;这里是退出条件
        total += term
        count += 1
        average = total/count
    # 返回一个namedtuple,包含count和average两个字段。在python3.3前,如果生成器返回值,会报错
    return Result(count, average)

>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(20) # 并没有返回值
>>> coro_avg.send(30)
>>> coro_avg.send(40)
>>> try:
...     coro_avg.send(None)
... except StopIteration as exc:
...     result = exc.value
...
>>> result
Result(count=3, average=30)

  这里获取返回值的方法很繁琐,下面引出yield from。

yield from 初解

yield from 结果会在内部自动捕获StopIteration 异常。这种处理方式与 for 循环处理StopIteration异常的方式一样。 对于yield from 结构来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from 表达式的值。

在函数外部不能使用yield from(yield也不行)。

yield from 是 Python3.3 后新加的语言结构。和其他语言的await关键字类似,它表示:*在生成器 gen 中使用 yield from subgen()时,subgen 会获得控制权,把产出的值传给gen的调用方,即调用方可以直接控制subgen。于此同时,gen会阻塞,等待subgen终止,subgen是子生成器。

yield from 可用于简化for循环中的yield表达式。

例如:

def gen():
    for c in 'AB':
        yield c
    for i in range(1, 3):
        yield i

list(gen())
['A', 'B', '1', '2']

可以改写为:

def gen():
    yield from 'AB'
    yield from range(1, 3)
    
list(gen())
['A', 'B', '1', '2']

  还有一个稍微复杂点的例子

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
    for x in items:
        if isinstance(x, Iterable) and not isinstance(x, ignore_types):
            # yield from x(subgen)表达式对x对象做的第一件事是,调用iter(x),获取迭代器,所以要求x是可迭代对象,每次迭代的值为yield的右值。
            yield from flatten(x)  # 这里递归调用,如果flatten(x)中参数x是可迭代对象,继续分解
            print('委派器从yield from阻塞中还原')
        else:
            yield x  # 这里是每次迭代的值,直接传给调用者,跳过(不经过)委派者管道


items = [1, 2, [3, 4, [5, 6], 7], 8]

# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):  # 第一步yield的右值
    print(x)  # x为子生成器中每次迭代的
    print('调用者对委派器进行了一次迭代,委派器返回一个值')

items = ['Dave', 'Paula', ['Thomas', 'Lewis']]
for x in flatten(items):
    print(x)

  这里的注释看不懂先往下看,理解了yield from的委派器、管道作用就好理解了。

  上面的例子如果进行了断点调试的话,你看你会发现在子生成器(subgen)中,yield x直接能吧值传给最外层的for循环,这就涉及到下面要说的yield from是连接子生成器和调用者的一个通道

 yield from的作用

PEP380 的标题是 ”syntax for delegating to subgenerator“(把指责委托给子生成器的句法)。由此我们可以知道,yield from是可以实现嵌套生成器的使用。注意,使用 yield from 句法调用协程时,会自动预激。

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,使两者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程添加异常处理的代码。

yield from 包含几个概念:

  • 委派生成器

包含yield from 表达式的生成器函数

  • 子生成器

从yield from 部分获取的生成器。

  • 调用方

调用委派生成器的客户端(调用方)代码

这个示意图(图一)是对yield from 的调用过程

上面的图难以理解可以看下面这个简易图

  这两张图说明了,委派生成器可以理解为管道,send和yield直接在调用方和子生成器(subgen)进行交互,send可以直接把值由调用方“穿过(不经过)“委派生成器直接传递给子生成器,子生成器的yield也可以把yield右边的值穿过(不经过)委派生成器直接传递给调用者。

  yield from运行的全过程可以概括为:委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给字生成器,子生成器再把产出的值发送给调用方。子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,这时委派生成器恢复。

  下面看一个例子来理解yield from的运行方式,这段代码从一个字典中读取男生和女生的身高和体重。然后把数据传给之前定义的 averager 协程,最后生成一个报告。

from collections import namedtuple

Result = namedtuple('Result', 'count average')

# 子生成器
# 这个例子和上边示例中的 averager 协程一样,只不过这里是作为子生成器使用
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        # main 函数发送数据到这里 
        term = yield  # yield右边如果有数据,则这个数据会直接传递给main,main中委派生成器迭代一次,本次迭代的值是yield的右值
        if term is None: # 终止条件
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average) # 返回的Result 会成为grouper函数中yield from表达式的值


# 委派生成器
def grouper(results, key):
     # 这个循环每次都会新建一个averager 实例,每个实例都是作为协程使用的生成器对象
    while True:
        # grouper 发送的每个值都会经由yield from 处理,通过管道传给averager 实例。grouper会在yield from表达式处暂停,等待averager实例处理客户端发来的值。averager实例运行完毕后,返回的值绑定到results[key] 上。while 循环会不断创建averager实例,处理更多的值。
        results[key] = yield from averager()


# 调用方
def main(data):
    results = {}
    for key, values in data.items():
        # group 是调用grouper函数得到的生成器对象,传给grouper 函数的第一个参数是results,用于收集结果;第二个是某个键
        group = grouper(results, key)
        next(group)
        for value in values:
            # 把各个value传给grouper 传入的值最终到达averager函数中;
            # grouper并不知道传入的是什么,同时grouper实例在yield from处暂停
            group.send(value)
        # 把None传入groupper,传入的值最终到达averager函数中,导致当前实例终止。然后继续创建下一个实例。
        # 如果没有group.send(None),那么averager子生成器永远不会终止,委派生成器也永远不会在此激活,也就不会为result[key]赋值
        group.send(None)
    report(results)


# 输出报告
def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))


data = {
    'girls;kg':[40, 41, 42, 43, 44, 54],
    'girls;m': [1.5, 1.6, 1.8, 1.5, 1.45, 1.6],
    'boys;kg':[50, 51, 62, 53, 54, 54],
    'boys;m': [1.6, 1.8, 1.8, 1.7, 1.55, 1.6],
}

if __name__ == '__main__':
    main(data)

  执行结果为

6 boys  averaging 54.00kg
6 boys  averaging 1.68m
6 girls averaging 44.00kg
6 girls averaging 1.58m

  这断代码展示了yield from 结构最简单的用法。委派生成器相当于管道,所以可以把任意数量的委派生成器连接在一起—一个委派生成器使用yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个生成器。最终以一个只是用yield表达式的生成器(或者任意可迭代对象)结束。

 yield from 的意义

PEP380 分6点说明了yield from 的行为。

  • 子生成器产出的值(yield右值)都直接传给委派生成器的调用方(客户端代码)
  • 使用send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是None,那么会调用子生成器的 next()方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行(从yield from阻塞中恢复)。任何其他异常都会向上冒泡(向外层传递),传给委派生成器。
  • 生成器退出时,生成器(或子生成器)中的return expr 表达式会触发 StopIteration(expr) 异常抛出。
  • yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。
  • 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的throw()方法。如果调用throw()方法时抛出 StopIteration 异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡(有异常处理就处理,没处理继续上冒)。传给委派生成器。
  • 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用close() 方法,那么在子生成器上调用close() 方法,如果他有的话。如果调用close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。

PEP380 还有个说明:

In a generator, the statement

return value

is semantically equivalent to

raise StopIteration(value)

except that, as currently, the exception cannot be caught by except clauses within the returning generator.

这也就是为什么 yield from 可以使用return 来返回值而 yield 只能使用 try … except StopIteration … 来捕获异常的value 值。

yield 实现并发(协程)

会创建几辆出租车,每辆出租车会拉几个乘客,然后回家。出租车会首先驶离车库,四处徘徊,寻找乘客;拉到乘客后,行程开始;乘客下车后,继续四处徘徊。

import random
import collections
import queue
import argparse

DEFAULT_NUMBER_OF_TAXIS = 3
DEFAULT_END_TIME = 180
SEARCH_DURATION = 5
TRIP_DURATION = 20
DEPARTURE_INTERAVAL = 5

# time 是事件发生的仿真时间,proc 是出租车进程实例的编号,action是描述活动的字符串
Event = collections.namedtuple('Event', ['time', 'proc', 'action'])


# 开始 出租车进程
# 每辆出租车调用一次taxi_process 函数,创建一个生成器对象,表示各辆出租车的运营过程。
def taxi_process(ident, trips, start_time=0):
"""
每次状态变化时向创建事件,把控制权交给仿真器
:param ident: 出租车编号
:param trips: 出租车回家前的行程数量,接客数
:param start_time: 离开车库的时间
:return:
"""
time = yield Event(start_time, ident, 'leave garage') # 产出的第一个Event
for i in range(trips): # 每次行程都会执行一遍这个代码块
# 产出一个Event实例,表示拉到了乘客 协程在这里暂停 等待下一次send() 激活
time = yield Event(time, ident, 'pick up passenger')
# 产出一个Event实例,表示乘客下车 协程在这里暂停 等待下一次send() 激活
time = yield Event(time, ident, 'drop off passenger')
# 指定的行程数量完成后,for 循环结束,最后产出 'going home' 事件。协程最后一次暂停
yield Event(time, ident, 'going home')
# 协程执行到最后 抛出StopIteration 异常


def compute_duration(previous_action):
"""使用指数分布计算操作的耗时"""
if previous_action in ['leave garage', 'drop off passenger']:
# 新状态是四处徘徊
interval = SEARCH_DURATION
elif previous_action == 'pick up passenger':
# 新状态是开始行程
interval = TRIP_DURATION
elif previous_action == 'going home':
interval = 1
else:
raise ValueError('Unkonw previous_action: %s' % previous_action)
return int(random.expovariate(1 / interval)) + 1


# 开始仿真
class Simulator:

def __init__(self, procs_map):
self.events = queue.PriorityQueue() # 带优先级的队列 会按时间正向排序
self.procs = dict(procs_map) # 从获取的procs_map 参数中创建本地副本,为了不修改用户传入的值

def run(self, end_time):
"""
调度并显示事件,直到时间结束
:param end_time: 结束时间 只需要指定一个参数
:return:
"""
# 调度各辆出租车的第一个事件
for iden, proc in sorted(self.procs.items()):
first_event = next(proc) # 预激协程 并产出一个 Event 对象
self.events.put(first_event) # 把各个事件加到self.events 属性表示的 PriorityQueue对象中

# 此次仿真的主循环
sim_time = 0 # 把 sim_time 归0
while sim_time < end_time:
if self.events.empty(): # 事件全部完成后退出循环
print('*** end of event ***')
break
current_event = self.events.get() # 获取优先级最高(time 属性最小)的事件
sim_time, proc_id, previous_action = current_event # 更新 sim_time
print('taxi:', proc_id, proc_id * ' ', current_event)
active_proc = self.procs[proc_id] # 从self.procs 字典中获取表示当前活动的出租车协程
next_time = sim_time + compute_duration(previous_action)
try:
next_event = active_proc.send(next_time) # 把计算得到的时间发送给出租车协程。协程会产出下一个事件,或者抛出 StopIteration
except StopIteration:
del self.procs[proc_id] # 如果有异常 表示已经退出, 删除这个协程
else:
self.events.put(next_event) # 如果没有异常,把next_event 加入到队列
else: # 如果超时 则走到这里
msg = '*** end of simulation time: {} event pendding ***'
print(msg.format(self.events.qsize()))


def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, seed=None):
"""初始化随机生成器,构建过程,运行仿真程序"""
if seed is not None:
random.seed(seed) # 获取可复现的结果
# 构建taxis 字典。值是三个参数不同的生成器对象。
taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERAVAL)
for i in range(num_taxis)}
sim = Simulator(taxis)
sim.run(end_time)


if __name__ == '__main__':
# parser = argparse.ArgumentParser(description='Taxi fleet simulator.')
# parser.add_argument('-e', '--end-time', type=int,
# default=DEFAULT_END_TIME,
# help='simulation end time; default=%s' % DEFAULT_END_TIME)
# parser.add_argument('-t', '--taxis', type=int,
# default=DEFAULT_NUMBER_OF_TAXIS,
# help='number of taxis running; default = %s' % DEFAULT_NUMBER_OF_TAXIS)
# parser.add_argument('-s', '--seed', type=int, default=None,
# help='random generator seed (for testing)')
#
# args = parser.parse_args()
main()

运行程序

# -s 3 参数设置随机生成器的种子,以便调试的时候随机数不变,输出相同的结果
python taxi_sim.py -s 3

输出结果如下图

从结果我们可以看出,3辆出租车的行程是交叉进行的。不同颜色的箭头代表不同出租车从乘客上车到乘客下车的跨度。

从结果可以看出:

  • 出租车每5隔分钟从车库出发
  • 0 号出租车2分钟后拉到乘客(time=2),1号出租车3分钟后拉到乘客(time=8),2号出租车5分钟后拉到乘客(time=15)
  • 0 号出租车拉了两个乘客
  • 1 号出租车拉了4个乘客
  • 2 号出租车拉了6个乘客
  • 在此次示中,所有排定的事件都在默认的仿真时间内完成

我们先在控制台中调用taxi_process 函数,自己驾驶一辆出租车,示例如下:

In [1]: from taxi_sim import taxi_process
# 创建一个生成器,表示一辆出租车 编号是13 从t=0 开始,有两次行程
In [2]: taxi = taxi_process(ident=13, trips=2, start_time=0) 

In [3]: next(taxi) # 预激协程
Out[3]: Event(time=0, proc=13, action='leave garage')

# 发送当前时间 在控制台中,变量_绑定的是前一个结果
# _.time + 7 是 0 + 7
In [4]: taxi.send(_.time+7) 
Out[4]: Event(time=7, proc=13, action='pick up passenger') # 这个事件有for循环在第一个行程的开头产出

# 发送_.time+12 表示这个乘客用时12分钟
In [5]: taxi.send(_.time+12)
Out[5]: Event(time=19, proc=13, action='drop off passenger')

# 徘徊了29 分钟
In [6]: taxi.send(_.time+29)
Out[6]: Event(time=48, proc=13, action='pick up passenger')

# 乘坐了50分钟
In [7]: taxi.send(_.time+50)
Out[7]: Event(time=98, proc=13, action='drop off passenger')

# 两次行程结束 for 循环结束产出'going home'
In [8]: taxi.send(_.time+5)
Out[8]: Event(time=103, proc=13, action='going home')

# 再发送值,会执行到末尾 协程返回后 抛出 StopIteration 异常
In [9]: taxi.send(_.time+10)
---------------------------------------------------------------------------
StopIteration                            Traceback (most recent call last)
<ipython-input-9-d775cc8cc079> in <module>()
----> 1 taxi.send(_.time+10)

StopIteration:

在这个示例中,我们用控制台模拟仿真主循环。从taxi协程中产出的Event实例中获取 .time 属性,随意加一个数,然后调用send()方法发送两数之和,重新激活协程。

在taxi_sim.py 代码中,出租车协程由 Simulator.run 方法中的主循环驱动。

Simulator 类的主要数据结构如下:

self.events

PriorityQueue 对象,保存Event实例。元素可以放进PriorityQueue对象中,然后按 item[0](对象的time 属性)依序取出(按从小到大)。

self.procs

一个字典,把出租车的编号映射到仿真过程的进程(表示出租车生成器的对象)。这个属性会绑定前面所示的taxis字典副本。

优先队列是离散事件仿真系统的基础构件:创建事件的顺序不定,放入这种队列后,可以按各个事件排定的顺序取出。

比如,我们把两个事件放入队列:

Event(time=14, proc=0, action='pick up passenger')
Event(time=10, proc=1, action='pick up passenger')

这个意思是 0号出租车14分拉到一个乘客,1号出租车10分拉到一个乘客。但是主循环获取的第一个事件将是

Event(time=10, proc=1, action=‘pick up passenger’)

下面我们分析一下仿真系统的主算法–Simulator.run 方法。

  1. 迭代表示各辆出租车的进程
    • 在各辆出租车上调用next()函数,预激协程。
    • 把各个事件放入Simulator类的self.events属性中。
  2. 满足 sim_time < end_time 条件是,运行仿真系统的主循环。
    • 检查self.events 属性是否为空;如果为空,跳出循环
    • 从self.events 中获取当前事件
    • 显示获取的Event对象
    • 获取curent_event 的time 属性,更新仿真时间
    • 把时间发送给current_event 的pro属性标识的协程,产出下一个事件
    • 把next_event 添加到self.events 队列中,排定 next_event

我们代码中 while 循环有一个else 语句,仿真系统到达结束时间后,代码会执行else中的语句。

这个示例主要是想说明如何在一个主循环中处理事件,以及如何通过发送数据驱动协程,同时解释了如何使用生成器代替线程和回调,实现并发。

本文参考http://blog.gusibi.com/post/python-coroutine-yield-from/

 

posted @ 2018-12-10 12:56  醉生卐梦死  阅读(674)  评论(0编辑  收藏  举报