出租车队运营仿真

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

程序解释

程序的输出示例:

创建 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 时。

主程序

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

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

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

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

  1. 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。

代码

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

    #define LEN_SERIAL_NUMBER (12) void C_CosemObjMeterInfo::SetSerialNum(U08 u08MeterId, U64 u64SerialN ...

  2. [CF914D]Sum the Fibonacci

    题目   点这里看题目. 分析   我们先放宽条件,重新定义五元组\((a,b,c,d,e)\)如下:   1.\(1\le a,b,c,d,e\le n\).   2.\(s_a\&s_b= ...

  3. 贪吃蛇游戏(printf输出C语言版本)

    这一次我们应用printf输出实现一个经典的小游戏—贪吃蛇,主要难点是小蛇数据如何存储.如何实现转弯的效果.吃到食物后如何增加长度. 1 构造小蛇 首先,在画面中显示一条静止的小蛇.二维数组canva ...

  4. [CentOS 7]挂载ntfs格式U盘

    在我们将U盘插入装有CentOS的系统时,经常会出现如图所示的错误提示.这是因为linux系统并不能兼容NTFS的文件系统.其解决方法如下(建议先进入root模式): 1.首先下载"ntfs ...

  5. phpmyadmin通过慢查询日志getshell连载(二)

    这是phpmyadmin系列渗透思路的第二篇文章,前面一篇文章阐述了通过全局日志getshell,但是还有一个日志可以getshell,那就是本次实验的慢查询日志,操作类似,毕竟实战中多一条路就多一次 ...

  6. MySQL5.7.X 的下载和安装

    1 MySQL的下载 这里是mysql5.7.30的版本下载地址 https://dev.mysql.com/downloads/mysql/5.7.html#downloads 根据自己电脑选择合适 ...

  7. vue-admin-template搭建后台管理系统的学习(一)

    首先我们来看看这个基础模版的目录结构 ├── build // 构建相关  ├── config // 配置相关├── src // 源代码│   ├── api // 所有请求│   ├── ass ...

  8. mybatis读写分离

    mybatis读写分离实现方式有很多种,当然如果没有太过复杂的处理,可以使用阿里云数据库自带的读写分离连接,那样会更加简洁.本文主要对mybatis实现读写分离.主要的实现方式有一下四种: 方案1 通 ...

  9. 动力节点 mysql 郭鑫 34道经典的面试题三

    1.第十五题 15.列出受雇日期早于其直接上级的所有员工编号.姓名.部门名称 思路一:第一步将emp a看成员工表,将emp b 看成领导表,员工表的mgr字段应该等于领导表的主键字段 mysql&g ...

  10. java命令行输入参数

    Java命令行输入参数 代码用例:命令行输入参数,并进行加法运算. public class Demo01 { public static void main(String[] args) { for ...