出租车队运营仿真

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

程序解释

程序的输出示例:

创建 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 队列中。

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

python 并发专题(五):离散事件仿真(事件循环生成器)的更多相关文章

  1. python并发编程之进程2(管道,事件,信号量,进程池)

    管道 Conn1,conn2 = Pipe() Conn1.recv() Conn1.send() 数据接收一次就没有了 from multiprocessing import Process,Pip ...

  2. python 并发专题(一):并发基础相关概念,术语等

    一.线程 1.概念 线程是程序执行流的最小执行单位,是行程中的实际运作单位. 进程是一个动态的过程,是一个活动的实体.简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执 ...

  3. python 并发专题(十四):asyncio (三)实战

    https://www.cnblogs.com/wongbingming/p/9124142.html 在实战中,将会用到以下知识点: 多线程的基本使用 Queue消息队列的使用 Redis的基本使用 ...

  4. python 并发专题(十三):asyncio (二) 协程中的多任务

    . 本文目录# 协程中的并发 协程中的嵌套 协程中的状态 gather与wait . 协程中的并发# 协程的并发,和线程一样.举个例子来说,就好像 一个人同时吃三个馒头,咬了第一个馒头一口,就得等这口 ...

  5. python 并发专题(二):python线程以及线程池相关以及实现

    一 多线程实现 线程模块 - 多线程主要的内容:直接进行多线程操作,线程同步,带队列的多线程: Python3 通过两个标准库 _thread 和 threading 提供对线程的支持. _threa ...

  6. python 并发专题(十三):asyncio (一) 初识

    https://www.cnblogs.com/wongbingming/p/9095243.html . 本文目录# 如何定义/创建协程 asyncio的几个概念 学习协程是如何工作的 await与 ...

  7. python 并发专题(四):yield以及 yield from

    一.yield python中yield的用法很像return,都是提供一个返回值,但是yield和return的最大区别在于,return一旦返回,则代码段执行结束,但是yield在返回值以后,会交 ...

  8. python 并发专题(六):协程相关函数以及实现(gevent)

    文档资源 http://sdiehl.github.io/gevent-tutorial/ 一.协程实现 线程和协程 既然我们上面也说了,协程也被称为微线程,下面对比一下协程和线程: 线程之间需要上下 ...

  9. python 并发专题(三):进程以及进程池相关以及实现

    一.多进程实现 multiprocess.process模块 process类 Process([group [, target [, name [, args [, kwargs]]]]]),由该类 ...

随机推荐

  1. git clone 时注意点

    环境: 在公司访问外网需要设置代理,另外,在公司局域网内架设了一台 GIT 服务器. 在使用 git clone 时,不能设置成 git 使用代理: git config --global http. ...

  2. 消息队列——RabbitMQ的基本使用及高级特性

    文章目录 一.引言 二.基本使用 1. 简单示例 2. work queue和公平消费消息 3. 交换机 三.高级特性 1. 消息过期 2. 死信队列 3. 延迟队列 4. 优先级队列 5. 流量控制 ...

  3. LNMP 环境更换Nginx 服务器为Tengine

    本人之前所使用 LNMP 环境一直是原生的Nginx服务器,最近几天看了好多大网站使用 淘宝团队基于Nginx开发的 Tengine 决定给自己的虚拟机也装个玩玩. 关于Tengine的介绍就不多说了 ...

  4. C#数据结构与算法系列(七):约瑟夫问题(Josephu)

    1.介绍 Josephu问题为:设编号为1.2....n的n个人围坐在一圈,约定编号为k(1<=k<=n) 的人从1开始报数, 数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人 ...

  5. ubuntu启动打开终端快捷键

    ubuntu启动打开终端快捷键 CTRL+ALT+T

  6. 01 . 容器编排简介及Kubernetes核心概念

    Kubernetes简介 Kubernetes是谷歌严格保密十几年的秘密武器-Borg的一个开源版本,是Docker分布式系统解决方案.2014年由Google公司启动. Kubernetes提供了面 ...

  7. Spark HA搭建

    正文 下载Spark版本,这版本又要求必须和jdk与hadoop版本对应. http://spark.apache.org/downloads.html tar -zxvf 解压到指定目录,进入con ...

  8. ceph rbd块存储挂载及文件存储建立

    一.rbd块存储挂载 1 创建一个OSD pool # ceph osd pool create rbd1 128 查询存储空间使用 # ceph df GLOBAL: SIZE AVAIL RAW ...

  9. 深入解读Dictionary

    Dictionary<TKey,TValue>是日常.net开发中最常用的数据类型之一,基本上遇到键值对类型的数据时第一反应就是使用这种散列表.散列表特别适合快速查找操作,查找的效率是常数 ...

  10. Flask项目实战:创建电影网站(2)

    flask网站制作后台时候常见流程总结 安利一个神神器: 百度脑图PC版 创建数据库 下面是创建User数据库,需要导入db库 #coding:utf8 from flask import Flask ...