#Python# #实验# #教程#

前两篇我们已经介绍了python 协程的使用和yield from 的原理,这一篇,我们用一个例子来揭示如何使用协程在单线程中管理并发活动。

什么是离散事件仿真

Wiki上的定义是:

离散事件仿真将系统随时间的变化抽象成一系列的离散时间点上的事件,通过按照事件时间顺序处理事件来演进,是一种事件驱动的仿真世界观。离散事件仿真将系统的变化看做一个事件,因此系统任何的变化都只能是通过处理相应的事件来实现,在两个相邻的事件之间,系统状态维持前一个事件发生后的状态不变。

人话说就是一种把系统建模成一系列事件的仿真系统。在离散事件仿真中,仿真“钟”向前推进的量不是固定的,而是直接推进到下一个事件模型的模拟时间。

假设我们抽象模拟出租车的运营过程,其中一个事件是乘客上车,下一个事件则是乘客下车。不管乘客做了5分钟还是50分钟,一旦下车,仿真钟就会更新,指向此次运营的结束时间。

事件?是不是想到了协程!

协程恰好为实现离散事件仿真提供了合理的抽象。

第一门面向对象的语音 Simula 引入协程这个概念就是为了支持仿真。

Simpy 是一个实现离散事件仿真的Python包,通过一个协程表示离散事件仿真系统的各个进程。

出租车对运营仿真

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

徘徊和行程所用的时间使用指数分布生成,我们将时间设为分钟数,以便显示清楚。

完整代码如下:(taxi_sim.py)




用python做仿真 python 仿真_python


运行程序,


用python做仿真 python 仿真_生成器_02


输出结果如下图


用python做仿真 python 仿真_Python_03


从结果我们可以看出,3辆出租车的行程是交叉进行的。不同颜色的箭头代表不同出租车从乘客上车到乘客下车的跨度。

从结果可以看出:

  • 出租车每5隔分钟从车库出发
  • 0 号出租车2分钟后拉到乘客(time=2),1号出租车3分钟后拉到乘客(time=8),2号出租车5分钟后拉到乘客(time=15)
  • 0 号出租车拉了两个乘客
  • 1 号出租车拉了4个乘客
  • 2 号出租车拉了6个乘客
  • 在此次示中,所有排定的事件都在默认的仿真时间内完成

我们先在控制台中调用taxi_process 函数,自己驾驶一辆出租车,示例如下:


用python做仿真 python 仿真_Python_04


在这个示例中,我们用控制台模拟仿真主循环。从taxi协程中产出的Event实例中获取 .time 属性,随意加一个数,然后调用send()方法发送两数之和,重新激活协程。

在taxi_sim.py 代码中,出租车协程由 Simulator.run 方法中的主循环驱动。

Simulator 类的主要数据结构如下:

self.events

PriorityQueue 对象,保存Event实例。元素可以放进PriorityQueue对象中,然后按 item[0](对象的time 属性)依序取出(按从小到大)。

self.procs

一个字典,把出租车的编号映射到仿真过程的进程(表示出租车生成器的对象)。这个属性会绑定前面所示的taxis字典副本。

优先队列是离散事件仿真系统的基础构件:创建事件的顺序不定,放入这种队列后,可以按各个事件排定的顺序取出。

比如,我们把两个事件放入队列:


用python做仿真 python 仿真_生成器_05


这个意思是 0号出租车14分拉到一个乘客,1号出租车10分拉到一个乘客。但是主循环获取的第一个事件将是 Event(time=10, proc=1, action='pick up passenger')

下面我们分析一下仿真系统的主算法--Simulator.run 方法。

  1. 迭代表示各辆出租车的进程
  • 在各辆出租车上调用next()函数,预激协程。
  • 把各个事件放入Simulator类的self.events属性中。
  1. 满足 sim_time < end_time 条件是,运行仿真系统的主循环。
  • 检查self.events 属性是否为空;如果为空,跳出循环
  • 从self.events 中获取当前事件
  • 显示获取的Event对象
  • 获取curent_event 的time 属性,更新仿真时间
  • 把时间发送给current_event 的pro属性标识的协程,产出下一个事件
  • 把next_event 添加到self.events 队列中,排定 next_event

我们代码中 while 循环有一个else 语句,仿真系统到达结束时间后,代码会执行else中的语句。

这个示例主要是想说明如何在一个主循环中处理事件,以及如何通过发送数据驱动协程,同时解释了如何使用生成器代替线程和回调,实现并发。

并发: 多个任务交替执行

并行: 多个任务同时执行

到这里 Python协程系列的三篇文章就结束了。

我们会看到,协程做面向事件编程时,会不断把控制权让步给主循环,激活并向前运行其他协程,从而执行各个并发活动。

协程一种协作式多任务:协程显式自主的把控制权让步给中央调度程序。

多线程实现的是抢占式多任务。调度程序可以在任何时刻暂停线程,把控制权交给其他线程