离散事件仿真库SimPy的执行逻辑介绍


参考文章:

  1. SimPy Discrete event simulation for Python
  2. python离散事件仿真库SimPy官方教程
  3. 离散事件仿真原理DES

内容介绍

离散事件仿真库Simpy的执行效率之所以很高,关键在于生成器的使用,在Python中通过yield来暂时停止协程中的子线程,再次调用时才从中断的位置开始。前面的文章《关于复制SimPy仿真环境的生成器的讨论》中我们介绍了生成器的相关特性。

本文基于简单的例子,介绍Simpy仿真环境的事件流过程。是如何控制调用生成器抛出的事件,以及如何控制仿真过程的结束。以下是一个公开找到的simpy仿真案例:

import simpy

def 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。并且在该环境中,将常见的事件命名为三类对象:processtimeoutevent

依次介绍 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 simpy

def 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 中获取下一个待执行的事件。

  • 47
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lins号丹

小小鼓励,满满动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值