python 并发专题(五):离散事件仿真(事件循环生成器)

出租车队运营仿真

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

程序解释

程序的输出示例:

 

 

             创建 3 辆出租车的输出示例。-s 3 参数设置随机数生成器的种子,这样在调试和演示时可以重复运行程序,输出相同的结果。不同颜色的箭头表示不同出租车的行程。

            最值得注意的一件事是,3 辆出租车的行程是交叉进行的。那些箭头是我加上的,为的是让你看清各辆出租车的行程:箭头从乘客上车时开始,到乘客下车后结束。有了箭头,能直观地看出如何使用协程管理并发的活动。

注意:

1.出租车每隔 5 分钟从车库中出发。
2. 0 号出租车 2 分钟后拉到乘客(time=2),1 号出租车 3 分钟后拉到乘客(time=8),2 号出租车 5 分钟后拉到乘客(time=15)。
3. 0 号出租车拉了两个乘客(紫色箭头):第一个乘客从 time=2 时上车,到 time=18 时下车;第二个乘客从 time=28 时上车,到
time=65 时下车——这是此次仿真中最长的行程。
4. 1 号出租车拉了四个乘客(绿色箭头),在 time=110 时回家。
5 2 号出租车拉了六个乘客(红色箭头),在 time=109 时回家。这辆车最后一次行程从 time=97 时开始,只持续了一分钟。
6.1 号出租车的第一次行程从 time=8 时开始,在这个过程中 2 号出租车离开了车库(time=10),而且完成了两次行程(那两个短的
红色箭头)。
7. 在此次运行示例中,所有排定的事件都在默认的仿真时间内(180分钟)完成;最后一次事件发生在 time=110 时。

主程序

taxis = {0: taxi_process(ident=0, trips=2, start_time=0),
         1: taxi_process(ident=1, trips=4, start_time=5),
         2: taxi_process(ident=2, trips=6, start_time=10)}
sim = Simulator(taxis)
sim.run(end_time)

 

taxi_process 协程,实现各辆出租车的活动

def taxi_process(ident, trips, start_time=0):  #每辆出租车调用一次 taxi_process 函数,创建一个生成器对象,表示各辆出租车的运营过程。
#ident 是出租车的编号(如上述运行示例中的 0、1、2);trips 是出租车回家之前的行程数量;
#start_time是出租车离开车库的时间。
"""每次改变状态时创建事件,把控制权让给仿真器""" time = yield Event(start_time, ident, 'leave garage') #产出的第一个 Event 是 'leave garage'。执行到这一行时,协程
#会暂停,让仿真主循环着手处理排定的下一个事件。
#需要重新激活这个进程时,主循环会发送(使用 send 方法)当前的仿真时间,赋值给time。
for i in range(trips): #每次行程都会执行一遍这个代码块。
time
= yield Event(time, ident, 'pick up passenger') #产出一个 Event 实例,表示拉到乘客了。
#协程在这里暂停。需要重新激活这个协程时,主循环会发送(使用 send 方法)当前的时间。 time
= yield Event(time, ident, 'drop off passenger') # 产出一个 Event 实例,表示乘客下车了。协程在这里暂停,等待主循环发送时间,然后重新激活 yield Event(time, ident, 'going home') #指定的行程数量完成后,for 循环结束,最后产出 'going home'事件。
#此时,协程最后一次暂停。仿真主循环发送时间后,协程重新激活;不过,这里没有把产出的值赋值给变量,因为用不到了。
# 出租车进程结束 协程执行到最后时,生成器对象抛出 StopIteration 异常。

这个协程用到了别处定义的两个对象:compute_delay 函数,返回单位为分钟的时间间隔;Event类,一个 namedtuple,定义方式如下:

Event = collections.namedtuple('Event', 'time proc action')

 

Simulator 类的主要数据结构如下

self.events
  PriorityQueue 对象,保存 Event 实例。元素可以放进(使用put 方法)PriorityQueue 对象中,然后按 item[0](即 Event 对象
的 time 属性)依序取出(使用 get 方法)。
self.procs
  一个字典,把出租车的编号映射到仿真过程中激活的进程(表示出租车的生成器对象)。这个属性会绑定前面所示的 taxis 字典副本。

Simulator.run 方法实现的算法:

(1) 迭代表示各辆出租车的进程。
  a. 在各辆出租车上调用 next() 函数,预激协程。这样会产出各辆出租车的第一个事件。
  b. 把各个事件放入 Simulator 类的 self.events 属性(队列)
中。
(2) 满足 sim_time < end_time 条件时,运行仿真系统的主循环。
  a. 检查 self.events 属性是否为空;如果为空,跳出循环。
  b. 从 self.events 中获取当前事件(current_event),即PriorityQueue 对象中时间值最小的 Event 对象。
  c. 显示获取的 Event 对象。
  d.获取 current_event 的 time 属性,更新仿真时间。

  e.把时间发给 current_event 的 proc 属性标识的协程,产出下一个事件(next_event)。
  f.把 next_event 添加到 self.events 队列中,排定next_event。

代码

#taxi_sim.py
class Simulator:

    def __init__(self, procs_map):
        self.events = queue.PriorityQueue() #保存排定事件的 PriorityQueue 对象,按时间正向排序。
        self.procs = dict(procs_map)# 获取的 procs_map 参数是一个字典(或其他映射),可是又从中构建一个字典,创建本地副本,
# 因为在仿真过程中,出租车回家后会从self.procs 属性中移除,而我们不想修改用户传入的对象。
def run(self, end_time):#run 方法只需要仿真结束时间(end_time)这一个参数。 """排定并显示事件,直到时间结束""" # 排定各辆出租车的第一个事件 for _, proc in sorted(self.procs.items()): #使用 sorted 函数获取 self.procs 中按键排序的元素;用不到键,因此赋值给 _。 first_event = next(proc) #调用 next(proc) 预激各个协程,向前执行到第一个 yield 表达式,做好接收数据的准备。产出一个 Event 对象。 self.events.put(first_event) #把各个事件添加到 self.events 属性表示的 PriorityQueue 对象中。
#如示例 16-20 中的运行示例,各辆出租车的第一个事件是 'leave garage'
# 这个仿真系统的主循环 sim_time = 0 #把 sim_time 变量(仿真钟)归零。 while sim_time < end_time: #这个仿真系统的主循环:sim_time 小于 end_time 时运行。 if self.events.empty(): #如果队列中没有未完成的事件,退出主循环。 print('*** end of events ***') break current_event = self.events.get() #获取优先队列中 time 属性最小的 Event 对象;这是当前事件(current_event)。 sim_time, proc_id, previous_action = current_event #拆包 Event 对象中的数据。这一行代码会更新仿真钟 sim_time,对应于事件发生时的时间。 print('taxi:', proc_id, proc_id * ' ', current_event) #显示 Event 对象,指明是哪辆出租车,并根据出租车的编号缩进。 active_proc = self.procs[proc_id] #从 self.procs 字典中获取表示当前活动的出租车的协程。 next_time = sim_time + compute_duration(previous_action) #调用 compute_duration(...) 函数,传入前一个动作
#(例如,'pick up passenger'、'drop off passenger' 等),
#把结果加到 sim_time 上,计算出下一次活动的时间。
try: next_event = active_proc.send(next_time) #把计算得到的时间发给出租车协程。
#协程会产出下一个事件(next_event),或者抛出 StopIteration 异常(完成时)
except StopIteration:
                del self.procs[proc_id]  #如果抛出了 StopIteration 异常,从 self.procs 字典中删除那个协程。
            else:
                self.events.put(next_event)  #否则,把 next_event 放入队列中。
        else:  #如果循环由于仿真时间到了而退出,显示待完成的事件数量(有时可能碰巧是零)
            msg = '*** end of simulation time: {} events pending ***'
            print(msg.format(self.events.qsize()))

 

注意,
1.主 while 循环有一个 else 语句,报告仿真系统由于到达结束时间而结束,而不是由于没有事件要处理而结束。
2.靠近主 while 循环底部那个 try 语句把 next_time 发给当前的出租车进程,尝试获取下一个事件(next_event),如果成功,执
行 else 块,把 next_event 放入 self.events 队列中。

这个示例的要旨是说明如何在一个主循环中处理事件,以及如何通过发送数据驱动协程。

posted @ 2020-04-17 16:51  秋华  阅读(1698)  评论(0编辑  收藏  举报