使用协程做离散事件仿真。
前面对yield from的理解花了好长时间取理解,好几天想着想着天就亮了,想到后面感觉脑子都快炸了,现在总算稍微有点明白了。
新开一篇随笔,记录我对书中离散事件仿真的理解,按照说中的说法,如果这篇理解好了,能够让让我更好的理解asyncio,Twisted,Tornado等库如何在但线程中管理多个并发活动。
感觉书本学习就是找出书中的错误。
先给自己两个巴掌,基础知道出现了一个大漏洞:
sorted函数:
In [2]: sorted? Signature: sorted(iterable, /, *, key=None, reverse=False) Docstring: Return a new list containing all items from the iterable in ascending order. A custom key function can be supplied to customize the sort order, and the reverse flag can be set to request the result in descending order. Type: builtin_function_or_method
我一直认为sorted返回的是一个全新的对象列表,是全新的,但是确实浅拷贝,在这次测试中,发现了一个问题,还好及时学习了。
In [41]: l = ([1,1],[2,2]) In [42]: l1 = sorted(l) In [43]: l[0] is l1[0] Out[43]: True
In [47]: l == l1 Out[47]: False In [48]: l Out[48]: ([1, 1], [2, 2]) In [49]: l1 Out[49]: [[1, 1], [2, 2]] In [50]: l == l1 Out[50]: False In [51]: l is l1 Out[51]: False
其实sorted在运行的时候,应该首相对可迭代对象进行了list,但list其实是对该对象列表话操作以后,里面的元素都是浅拷贝。
In [52]: l Out[52]: ([1, 1], [2, 2]) In [53]: l2 = list(l) In [54]: l2 Out[54]: [[1, 1], [2, 2]] In [55]: l2[0] is l[0] Out[55]: True
基础没学好,重新温习一下吧。
下面进入书中内容。
书中主要模拟了一个出租车公司的运行情况。
import random import collections import queue import argparse import time import inspect # 定义一些常量与默认参数 DEFAULT_NUMBER_OF_TAXIS = 3 # 默认出租车数量 DEFAULT_END_TIME = 180 # 默认一次出车事件单位应该是(minute) SEARCH_DURATION = 5 TRIP_DURATION = 20 DEPARTURE_INTERVAL = 5 # 三个参数分别为事件,车辆编号,发现的情况 Event = collections.namedtuple('Event','time proc action') # 一个车辆的轨迹 def taxi_process(ident, trips, start_time=0): """每次状态变化时向仿真程序产出一个事件 indent为车辆编号,trips为做几单生意 """ time = yield Event(start_time, ident, 'leave garage') # 从车库出发 # 开始做生意了 for i in range(trips): time = yield Event(time, ident, 'pick up passenger') time = yield Event(time, ident, 'drop off passenger') # 打包回家了 yield Event(time, ident, 'going home') # BEGIN TAXI_SIMULATOR class Simulator: def __init__(self, proc_map): self.events = queue.PriorityQueue() # 事件优先队列 self.procs = dict(proc_map) # 创建出租车对的副本 def run(self, end_time): '''调度并显示事件,知道事件结束''' # 调度各辆出租车的第一个事件 for _, proc in sorted(self.procs.items()): # 对出租车副本进行排序,按照k默认key进行过排序 # 上面这个sorted我觉的是多余的,我经过了测试放和不放都一样。 # 本来放入的队列就是优先队列,没必要对各个车辆协程根据key就是车辆编号进行排序 first_event = next(proc) # 预激每个车辆协程,取出每个车辆的初始event self.events.put(first_event) # 优先队列放入各个enent,默认按照time大小放入。 # 此次仿真的主循环 sim_time = 0 # 初始化开始时间 while sim_time < end_time: # 监测时间是否超时。 '''从这个循环一取一方可以看出来,如果没有车辆回家,事件队列里面的消息数量,一致就是 车队车辆的数量,每一辆车必须把最新的情况,放入事件队列里面。 ''' if self.events.empty(): print('*** end of events ***') # 事件队列没有消息了,说明没有车辆在路上了。 break # 退出 current_event = self.events.get() # 按照event第一元素的大小,从第一元素时间取出事件。 sim_time, proc_id, previous_action = current_event # 把事件元祖拆包取值 print('taxi:', proc_id * ' ', current_event) # 屏幕显示显示该事件车辆的状况 active_proc = self.procs[proc_id] # 取出该事件发生的时候的车辆 next_time = sim_time + computer_duration(previous_action) # 根据算法算出下次时间 try: next_event = active_proc.send(next_time) # 发送给车辆协议下次运行的时间,获取返回的事件 except StopIteration: # 如果没有yield产出,接受到StopIteration,说明该车已经计划出车任务完成。 del self.procs[proc_id] # 从车队副本里面删除这辆车 else: self.events.put(next_event) # 把事件放入事件容器里面。 else: msg = '*** end of simulation time: {} events pending ***' # 当while循环正常正常,说明sim_time>end_time print(msg.format(self.events.qsize())) # 上报没有完成的任务车辆。 def computer_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('Unknown previous_action: %s ' % previous_action) # 报错 return int(random.expovariate(1/interval) +1) # 这个一个非常有意思的随机生成符合指数分布的随机数 def main(end_time=DEFAULT_END_TIME, num_taxis=DEFAULT_NUMBER_OF_TAXIS, seed=None): if seed is not None: # 设置随机数的规则定位 random.seed(seed) # taxi_process参数第一个车辆编号, 第二个做几单生意,第三个初始开始时间 taxis = {i: taxi_process(i, (i+1)*2, i*DEPARTURE_INTERVAL) for i in range(num_taxis)} # 新建一个车队,感觉这个车队,这样的安排其实不合理,后面的车子解的单太多了 sim = Simulator(taxis) # 车子放去模拟器中 sim.run(end_time) # 车子跑起来,设定结束时间。 if __name__ == '__main__': # argparse是一个非常强大的对脚本输入参数的解释与筛选工具 parser = argparse.ArgumentParser( description='Taxi fleet simulator') # -e短名 --end_time长名 接收参数为int,默认参数为DEFAULT_END_TIME,还有help信息 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=f'number of taxis running; default ' f'= {DEFAULT_NUMBER_OF_TAXIS}') parser.add_argument('-s', '--seed', type=int, default=None, help='random generator seed (for testing)') args = parser.parse_args() # 解析参数 # 通过属性(长名)获取具体参数 main(args.end_time, args.taxis, args.seed)
shijianzhongdeMacBook-Pro:第十六章 shijianzhong$ python3 taxi_sim.py -h usage: taxi_sim.py [-h] [-e END_TIME] [-t TAXIS] [-s SEED] Taxi fleet simulator optional arguments: -h, --help show this help message and exit -e END_TIME, --end_time END_TIME simulation end time; default = 180 -t TAXIS, --taxis TAXIS number of taxis running; default = 3 -s SEED, --seed SEED random generator seed (for testing) shijianzhongdeMacBook-Pro:第十六章 shijianzhong$ python3 taxi_sim.py -t 3 -e 200 taxi: Event(time=0, proc=0, action='leave garage') taxi: Event(time=1, proc=0, action='pick up passenger') taxi: Event(time=5, proc=1, action='leave garage') taxi: Event(time=10, proc=2, action='leave garage') taxi: Event(time=11, proc=0, action='drop off passenger') taxi: Event(time=11, proc=1, action='pick up passenger') taxi: Event(time=18, proc=0, action='pick up passenger') taxi: Event(time=23, proc=2, action='pick up passenger') taxi: Event(time=41, proc=0, action='drop off passenger') taxi: Event(time=42, proc=2, action='drop off passenger') taxi: Event(time=43, proc=0, action='going home') taxi: Event(time=54, proc=2, action='pick up passenger') taxi: Event(time=64, proc=2, action='drop off passenger') taxi: Event(time=69, proc=1, action='drop off passenger') taxi: Event(time=70, proc=1, action='pick up passenger') taxi: Event(time=73, proc=2, action='pick up passenger') taxi: Event(time=77, proc=2, action='drop off passenger') taxi: Event(time=82, proc=1, action='drop off passenger') taxi: Event(time=86, proc=1, action='pick up passenger') taxi: Event(time=97, proc=2, action='pick up passenger') taxi: Event(time=102, proc=1, action='drop off passenger') taxi: Event(time=109, proc=1, action='pick up passenger') taxi: Event(time=112, proc=1, action='drop off passenger') taxi: Event(time=119, proc=2, action='drop off passenger') taxi: Event(time=120, proc=1, action='going home') taxi: Event(time=128, proc=2, action='pick up passenger') taxi: Event(time=145, proc=2, action='drop off passenger') taxi: Event(time=155, proc=2, action='pick up passenger') taxi: Event(time=162, proc=2, action='drop off passenger') taxi: Event(time=164, proc=2, action='going home') *** end of events *** shijianzhongdeMacBook-Pro:第十六章 shijianzhong$
简单的来插入说明一下这个random.expovariate的指数分布,有意思的。
In [83]: l = [random.expovariate(1/20) for i in range(10000)] In [84]: sum(l)/len(l) Out[84]: 19.71606700143721 In [85]: len([i for i in l if i >=20]) Out[85]: 3654 In [86]: l = [random.expovariate(1/20) for i in range(10000)] In [87]: sum(l)/len(l) Out[87]: 20.075195351365277 In [88]: len([i for i in l if i >=20]) Out[88]: 3666 In [89]: l = [random.expovariate(1/20) for i in range(10000)] In [90]: sum(l)/len(l) Out[90]: 20.05689060176687 In [91]: len([i for i in l if i >=20]) Out[91]: 3682
最后真的很感概这个逻辑的厉害,我觉的这里比较厉害的是第一用到了优先队列,避免了很多自己写逻辑的麻烦,每次出来的事件都是最靠前的时间。
第二个是模型的设计,每个车辆是个协程,每个事件都是yield生成的。
很厉害的是把事件放入优先队列里面,事件里面有一个车辆ID,等取出最新生成的事件以后,拿出取出对应的车队里面的车辆协程,生成下一次的事件放入优先队列。
一直到这个车子一天的工作任务结束。
这个优先队列就好比一个事件循环,worker一直从队列里面取出事件,分析后,拿到对应的协程,根据需求完成具体事件,完成后,把状态信息(事件)扔回队列,取下一个信息。
这个优先队列里面的信息都是代办事情,主进程只要守着那个队列,取出最紧要的任务的进行处理,每个出租车协程根据在优先队列里面保存的最新状态(事件),worked会对优先事件对应的每个出租车携程进行操作。
整个思路下来,我觉的这个模型的设置最牛逼,第二个是用了内置的优先队列。厉害的我的神,什么时候我能这么厉害就好了。