文章目录
- 内容介绍
- 详细执行逻辑分析
- 大致仿真流程
- Simpy核心类的细节
- Environment 类
- Event 类
- Process类(Event)
- 基于案例详细介绍仿真逻辑
- env.run() 方法逻辑
- env.process() 方法逻辑
参考文章:
- SimPy Discrete event simulation for Python
- python离散事件仿真库SimPy官方教程
- 离散事件仿真原理DES
内容介绍
离散事件仿真库Simpy的执行效率之所以很高,关键在于生成器的使用,在Python中通过yield来暂时停止协程中的子线程,再次调用时才从中断的位置开始。前面的文章《关于复制SimPy仿真环境的生成器的讨论》中我们介绍了生成器的相关特性。
本文基于简单的例子,介绍Simpy仿真环境的事件流过程。是如何控制调用生成器抛出的事件,以及如何控制仿真过程的结束。以下是一个公开找到的simpy仿真案例:
import simpydef main():env = simpy.Environment()env.process(traffic_light(env))env.run(until=140)print('simulation done')def traffic_light(env):while True:print(f'light green at :{env.now}')yield env.timeout(30)print(f'light yellow at :{env.now}')yield env.timeout(5)print(f'light red at :{env.now}')yield env.timeout(20)if __name__ == '__main__':main()
其中,先通过simpy创建了一个仿真环境,在环境中定义了一个 process方法,并将我们自定义的 traffic_light 作为参数传入该方法,然后运行之后,会不断地在仿真环境 env 调用 traffic_light 方法。返回结果如下:
light green at :0
light yellow at :30
light red at :35
light green at :55
light yellow at :85
light red at :90
light green at :110
simulation done
详细执行逻辑分析
大致仿真流程
上述案例的运行逻辑是:先通过 simpy.Environment() 创建仿真环境 env,然后在环境中添加一个事件 process(traffic_light(env)),事件调用了一个函数 traffic_light(env),该函数的 env 为指明该函数的仿真环境,函数下的事件 env.timeout(30) 推进的是 env 的仿真事件。
在调用函数 traffic_light 时,不断地打印输出相应内容,以及推进仿真事件,当仿真事件被推进到终止的时间点时 env.run(until=140),仿真结束;在还未到达结束事件之前,事件会基于上次中断的位置,一直抛出事件并推进仿真。
Simpy核心类的细节
Environment 类
创建的仿真环境 env 的内容:这是基于事件的模拟执行环境,随着仿真事件的步进,一步步地从一个事件推进到另一个事件来进行模拟。simpy.Environment() 可以传入仿真环境的初始时间(申明传入的值可以为 int 类型或 float 类型),若为空则默认初始时间为 0。并且在该环境中,将常见的事件命名为三类对象:process,timeout,event。
依次介绍 Event 类的属性和方法:
_now:仿真时间;_queue:当前待处理的事件,一个列表,元素为四元元组,元组的信息包含一个数值、事件优先级、事件ID、事件对象;_eid:迭代器生成事件ID,每次有事件安排进_queue时,都会赋予被安排事件一个iD;_active_proc存储当前正在执行的 Process 事件;BoundClass.bind_early(self)用于将 BoundClass 类型的属性绑定到环境实例上(以优化属性搜索时的开销,但会引入复杂性或影响代码的可维护性)(property) now():返回环境的仿真时间,被 @property 修饰为一个只读属性,可以直接视为一个属性进行访问,例如env.now无需加括号即可返回环境类的当前仿真时间;active_process():返回环境类当前执行中的 process 对象;schedule():基于事件给定的优先级和时间(now+delay),通过 heqppush 将事件(四元元组)插入到环境的事件队列_queue中,按照排程时间、优先级从小到大依次有序地从堆顶存放到堆底;peek():函数返回时间队列下一个待处理事件的排程时间,若没有顺位的事件,则返回 Infinity(无穷大的浮点数);step()函数处理下一个事件(从_queue的堆顶弹出下一个处理事件最早的事件),如果没有下一个事件则抛出 EmptySchedule 错误;run():环境类的入口函数,通过该函数让整个仿真环境运行起来,其中,参数until可以传入整数、浮点数以及事件对象。如果没有传入值,则仿真环境会运行至没有需要加工的事件;如果传入的是一个事件,则仿真环境会持续执行加工直到该事件被触发,如果在该事件被触发之前已经没有待处理事件了,则弹出 RuntimeError 错误;如果是一个数值,则仿真环境会持续执行直到仿真时间到达该数值,具体操作为:创建一个在 now+until 触发的截止事件,该事件的优先级为0,当运行到该事件时,则触发 StopSimulation,结束仿真。
因此,结束仿真事件的优先级为0,因此如上面的交通灯的案例,如果最后一个时间的触发的时间为140,且仿真环境的until=140,则最后一个事件不会被处理,仿真优先被终止。
Event 类
这是仿真中用来定义事件的类,每个事件都会在某个时间点发生。有三种状态:可能发生(未被触发,triggered=False)、正在发生(被触发,triggered=True)、已经发生(processed=True)。每个事件在初始化的时候都处于未被触发状态,被触发后会被安排进环境的 _queue 待加工队列,当被触发时,事件的 ok() 与 value() 会被修改。
依次介绍 Event 类的属性和方法:
_ok:布尔变量_defused:布尔变量_value:默认值为 PENDING 对象,__init__(env):定义事件所在的 仿真环境,初始化事件的callbacks为一个空列表;__repr__():返回事件的类名、事件id;(property) triggered():当事件被触发且它的callbacks即将被调用是返回 True,也就是self._value is not PENDING,如果没有被触发,则_value的值还是 PENDING(这是一个具体的对象,用来唯一标识_value);(property) processed():当事件已经完成(它的callbacks已经被调用)时返回 True,此时callbacks is None;(property) ok():返回_ok属性的值,当事件被触发时,改属性值为 True,如果事件在被触发之前被访问,则抛出 AttributeError 错误;(property) defused():返回对象是否存在_defused的判断,当一个事件失败后,则失败的事件的value会变成一个错误类型,在被调用该事件时被重新抛出;(defused.setter) defused():不论传入何值,都将事件的_defused赋值为 True;(property) value():当事件没有被触发,此时_value的值还是 PENDING,调用该方法会抛出 AttributeError,反之,则会返回_value的值;trigger():将事件的_ok和_value的值更新为传入事件的_ok和_value的值,并将本事件排进环境的_queue;succeed():触发事件,如果_value is not PENDING,则抛出 RuntimeError,说明事件在之前已经被触发了;反之,则修改_ok的值为 True,并对_value进行赋值,并将该时间排进环境的_queue;fail():触发事件失败,传入一个错误类型;如果事件未被触发,则将_ok赋值为 False,并将_value赋值为传入的错误类型,将该时间放入到环境的_queue中;__and__:返回一个Condition类对象,只有当前事件与 other 事件都加工完,这个Condition类对象被触发;__or__:返回一个Condition类对象,只有当前事件或 other 事件都加工完,这个Condition类对象被触发;
Process类(Event)
用来处理生成器产生的事件,也被成为协程,可以通过产生事件来暂停执行。process 在时间发生后,使用该事件的值恢复生成器的执行。对于失败的事件,生成器会抛出异常。
Process 本身也是一个事件,每当生成器返回或抛出异常,它就会被触发,它的 value 就是生成器的返回值或接到的抛出的异常情况。process 在执行过程中可以通过方法 interrupt 进行中断。
依次介绍 Process类的属性和方法:
env:指明 process对象所在的仿真环境;callbacks:空列表_generator:传入的生成器(带yield的函数);_target:初始化Initialize,开始 process 的执行;_desc():返回 process 的类名,以及生成器名;(property) target():返回 process 时间的_target,即 process 当前等待执行的事件,若 process 被中断,则该方法返回 None;(property) is_alive():返回 True 直到退出生成器;interrupt():提供一个原因并中断该 process 事件,如果该 process 事件已经被中断,则不能再中断;_resume():基于事件的值恢复process事件的执行(通过向生成器发送当前事件的值,并获取生成器的下一个事件),如果生成器退出,则 process 事件会基于生成器返回的value以及报错信息进行触发。
基于案例详细介绍仿真逻辑
基于上面提到的小案例,我们将详细分析这个案例在 simpy 中的底层执行逻辑。再回顾下代码:
import simpydef main():env = simpy.Environment()env.process(traffic_light(env))env.run(until=140)print('simulation done')def traffic_light(env):while True:print(f'light green at :{env.now}')yield env.timeout(30)print(f'light yellow at :{env.now}')yield env.timeout(5)print(f'light red at :{env.now}')yield env.timeout(20)if __name__ == '__main__':main()
根据 main() 函数,首先创建了仿真环境 Environment 对象,仿真环境的默认起始时间为 0。
env = simpy.Environment()
接着实例化一个 Process 对象,传入的参数为 traffic_light() 生成器,其中,env 传入该生成器是为了在生成器当中调用仿真环境的相关属性:
env.process(traffic_light(env))
接着传入 until=140 给仿真环境的 env.run() 函数,开始执行仿真过程,并且将仿真结束时间定为 140。
env.run(until=140)
env.run() 方法逻辑
具体进入 run(),首先传入的 until 不为空,此时判断传入的 until 是个事件还是个数值,现在传入的是数值,将 until 的值赋给局部变量 at(如果这个仿真结束时间小于等于仿真环境的当前时间,则抛出 ValueError),基于 untile 的值创建一个在 until+now 时候触发的事件,且该事件的优先级为最高优先级0,并给给这个事件的 callbacks 添加 StopSimulation.callback。
循环地 try 仿真环境的 step() 函数,当执行到仿真环境终止的 until 事件,抛出的 StopSimulation 错误会被捕获,仿真过程会被终止;当step() 函数中 heappop(self._queue) 取不出下一个待处理事件,则抛出 EmptySchedule 错误,捕获该错误时说明全部的事件都被执行结束,如果 until 事件还没被触发,则会抛出 RuntimeError 错误,错误信息如下;反之,则正常结束仿真。
raise RuntimeError(f'No scheduled events left but "until" event was not triggered: {until}')
env.process() 方法逻辑
run() 方法是关于如何把仿真环境当中的 _queue 按顺序执行完,而 process() 方法会基于生成器将一个个事件通过 env.schedule(self) 加入到仿真环境的 _queue,这里 schedule() 的默认优先级为 NORMAL=1,默认的 delay=0,说明生成器的事件是等到要处理时被加入到 _queue。
具体的,env.process(traffic_light(env)) 生成了一个 process 事件对象 Process(self, traffic_light(env)),在该 process 事件对象中,优先初始化了一个 Initialize() 事件,并以最高优先级0将该初始化事件加入到仿真环境的 _queue 当中,表示 process 事件开始执行。在初始化事件 Initialize().__init(),将传入的 Process 对象的 _resume 方法加入到初始化事件的 callbacks 列表当中(这个列表在 env.step() 当中会被执行,即在处理下一步事件时需要先处理的事件),因此在初始化事件当中,就需要先处理 Process 对象的 _resume 方法,它不断地从 process 中获取下一个待执行的事件。