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 队列中。
这个示例的要旨是说明如何在一个主循环中处理事件,以及如何通过发送数据驱动协程。
python 并发专题(五):离散事件仿真(事件循环生成器)的更多相关文章
- python并发编程之进程2(管道,事件,信号量,进程池)
管道 Conn1,conn2 = Pipe() Conn1.recv() Conn1.send() 数据接收一次就没有了 from multiprocessing import Process,Pip ...
- python 并发专题(一):并发基础相关概念,术语等
一.线程 1.概念 线程是程序执行流的最小执行单位,是行程中的实际运作单位. 进程是一个动态的过程,是一个活动的实体.简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执 ...
- python 并发专题(十四):asyncio (三)实战
https://www.cnblogs.com/wongbingming/p/9124142.html 在实战中,将会用到以下知识点: 多线程的基本使用 Queue消息队列的使用 Redis的基本使用 ...
- python 并发专题(十三):asyncio (二) 协程中的多任务
. 本文目录# 协程中的并发 协程中的嵌套 协程中的状态 gather与wait . 协程中的并发# 协程的并发,和线程一样.举个例子来说,就好像 一个人同时吃三个馒头,咬了第一个馒头一口,就得等这口 ...
- python 并发专题(二):python线程以及线程池相关以及实现
一 多线程实现 线程模块 - 多线程主要的内容:直接进行多线程操作,线程同步,带队列的多线程: Python3 通过两个标准库 _thread 和 threading 提供对线程的支持. _threa ...
- python 并发专题(十三):asyncio (一) 初识
https://www.cnblogs.com/wongbingming/p/9095243.html . 本文目录# 如何定义/创建协程 asyncio的几个概念 学习协程是如何工作的 await与 ...
- python 并发专题(四):yield以及 yield from
一.yield python中yield的用法很像return,都是提供一个返回值,但是yield和return的最大区别在于,return一旦返回,则代码段执行结束,但是yield在返回值以后,会交 ...
- python 并发专题(六):协程相关函数以及实现(gevent)
文档资源 http://sdiehl.github.io/gevent-tutorial/ 一.协程实现 线程和协程 既然我们上面也说了,协程也被称为微线程,下面对比一下协程和线程: 线程之间需要上下 ...
- python 并发专题(三):进程以及进程池相关以及实现
一.多进程实现 multiprocess.process模块 process类 Process([group [, target [, name [, args [, kwargs]]]]]),由该类 ...
随机推荐
- 前端Javascript效果汇总
1.DOM原生动态加载js <script type="text/javascript"> function loadJs(){ //得到html的头部dom var ...
- WPF 如何流畅地滚动ScrollViewer 简单实现下
看了看原生UWP的ScrollViewer,滑动很流畅(例如 开始菜单),但是WPF自带的ScrollViewer滚动十分生硬.. 突发奇想,今天来实现一个流畅滚动的ScrollViewer. 一.目 ...
- Linux服务器安装python3.6
CentOS 7上默认安装的python版本是2.7.5,系统自带的旧版本python被系统很多其他软件环境依赖,因此不能卸载原Python,直接选择Python3.6.5进行全新安装. 1 安装Py ...
- 使用python解线性矩阵方程(numpy中的matrix类)
这学期有一门运筹学,讲的两大块儿:线性优化和非线性优化问题.在非线性优化问题这里涉及到拉格朗日乘子法,经常要算一些非常变态的线性方程,于是我就想用python求解线性方程.查阅资料的过程中找到了一个极 ...
- cb29a_c++_STL_算法_查找算法_(2)search_n
cb29a_c++_STL_算法_查找算法_(2)search_n//比如:连续查找连续的n个8search_n(b,e,c,v),迭代器b,begin(),e,end().连续的c个vpos=sea ...
- 从一个计算器开始说起——C#中的工厂方法模式
工厂模式作为很常见的设计模式,在日常工作中出镜率非常高,程序员们一定要掌握它的用法哟,今天跟着老胡一起来看看吧. 举个例子 现在先让我们来看一个例子吧,比如,要开发一个简单的计算器,完成加减功能,通过 ...
- 深入浅出PyTorch(算子篇)
Tensor 自从张量(Tensor)计算这个概念出现后,神经网络的算法就可以看作是一系列的张量计算.所谓的张量,它原本是个数学概念,表示各种向量或者数值之间的关系.PyTorch的张量(torch. ...
- android屏幕适配的全攻略3-动态获取手机屏幕宽高及动态设置控件宽高
1.获取手机屏幕宽高: DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetr ...
- 9、ssh的集成方式2
1.在第一种的集成方式中,通过struts2-spring-plugin-2.1.8.1.jar这个插件让spring自动产生对应需要的action类,不需要在对应的spring.xml文件中进行配置 ...
- Python实用笔记 (19)面向对象编程——访问限制
在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑. 但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的na ...