简介:本资源是一份面向计算机专业本科生的课程设计级Python实践项目,聚焦电梯进程建模与调度算法实现,适用于操作系统、算法设计或GUI应用开发等课程学习与实训。压缩包共36个文件,含3个核心Python源码(myElevator.py、dispatch.py、myElevatorInterface.py)、1份完整Word版设计方案报告、18张UI界面截图(涵盖按钮状态、运行指示、楼层状态等)、1个PyQt5图标资源及LICENSE等辅助文件,整体19.32MB,结构清晰,模块职责分明——调度逻辑、界面交互、状态管理分层解耦。已有668人学习下载,读者可直接运行PyQt5 GUI程序观察多线程模拟的电梯响应过程,结合设计报告深入理解FCFS、SCAN等调度策略在进程抽象中的映射实现,并参考PNG素材快速复现界面效果,是理论联系实际、从代码到可视化的完整教学闭环案例。
1. 用 Python 模拟电梯调度,不是写个动画,而是建一个可验证的进程管理模型
很多人看到“电梯调度”第一反应是画个带楼层的 GUI 界面,拖动小方块上下跑——那只是演示,不是建模。真正有价值的电梯调度模拟,核心在于把「请求生成→就绪队列组织→CPU(即电梯轿厢)资源分配→执行时长控制→状态迁移」这一整套进程生命周期,用操作系统级逻辑映射到物理电梯行为上。它本质是一个单处理器、非抢占式、FCFS 就绪队列、带 I/O 阻塞(开门/关门/加减速)的受限进程调度器。你不需要控制真实电梯,但必须让每个“乘客请求”表现为一个有创建时间、目标楼层、等待时长、服务时长、阻塞状态的进程实体;让“电梯”本身是一个独占资源的调度主体,能按规则从就绪队列取任务、执行、切换、响应中断(如新召唤按钮按下)。这个模型对理解 Linux 进程调度、容器编排中的资源抢占、甚至 AGV 调度系统底层逻辑,都有直接迁移价值。适合刚学完操作系统原理想动手验证的学生,也适合需要快速构建轻量级调度原型的嵌入式或 IoT 开发者。
2. 为什么选 Python 而不是 C 或 Rust?关键在三类对象的抽象与解耦
2.1 电梯调度模型的三层抽象:请求、进程、调度器必须分离
真实电梯系统中,“某人在 3 楼按了上行键”是一个外部事件,它触发系统创建一个服务请求;该请求被接纳后,会绑定到某个电梯轿厢的执行上下文中,形成一个具有生命周期的“服务进程”;而多个进程如何排队、谁先走、是否被中断,则由独立的调度器决策。这三层不能揉在一起写成一个 while 循环。Python 的类封装和弱类型特性,恰好支持这种清晰分层:
ElevatorRequest类只管原始输入:timestamp(毫秒级创建时间)、source_floor、target_floor、direction('UP'/'DOWN')、is_internal(轿厢内按钮)ElevatorProcess类继承自threading.Thread,但重写run()为纯逻辑执行(不真启线程),包含state(READY/RUNNING/BLOCKED/TERMINATED)、remaining_time(模拟运行耗时)、wait_time(就绪队列等待时长)ElevatorScheduler类持有就绪队列(deque)、运行中进程引用、电梯当前状态(current_floor,direction,is_moving),并实现schedule()方法
提示:这里不用
multiprocessing.Process,因为真实电梯是单体设备,不存在跨核并行;也不用asyncio,因 FCFS 是确定性调度,无需协程切换开销。用threading.Thread子类仅作状态容器,避免引入真实并发干扰调度逻辑验证。
2.2 就绪队列必须用双端队列(deque),且严格按 FCFS 插入
FCFS(First-Come, First-Served)不是简单排序,而是插入顺序即执行顺序。若用list.append()+list.pop(0),每次弹出首元素都是 O(n) 时间复杂度,当请求量达千级时,调度延迟会失真。必须用collections.deque,其append()和popleft()均为 O(1):
from collections import deque class ElevatorScheduler: def __init__(self): self.ready_queue = deque() # 关键:用 deque 实现 O(1) FCFS 队列 self.running_process = None self.current_floor = 1 self.direction = 'IDLE' def add_request(self, req: ElevatorRequest): # 创建对应进程,并按到达时间顺序入队 proc = ElevatorProcess(req) self.ready_queue.append(proc) # O(1) 插入尾部2.2.1 为什么不能用 heapq 或 sorted list?
heapq默认最小堆,若按req.timestamp排序,看似合理,但 FCFS 要求完全忽略优先级,只认到达先后。一旦两个请求timestamp相同(毫秒级精度下极易发生),heapq的稳定排序无法保证插入顺序,会导致调度结果不可复现。而deque天然保序,且无比较开销。
2.3 进程状态机必须显式定义,且阻塞态需区分类型
电梯进程的阻塞不是简单的time.sleep(),它分三种可观察类型:
| 阻塞类型 | 触发条件 | 持续时间 | 可否被抢占 | 对调度器影响 |
|---|---|---|---|---|
| DOOR_OPENING | 到达目标楼层前 | 固定 2s | 否(物理限制) | 调度器必须等待,不处理新请求 |
| DOOR_CLOSING | 开门结束后 | 固定 1.5s | 否 | 同上 |
| ACCELERATION/DECELERATION | 启动/停止瞬间 | 与距离相关(每层 0.8s) | 否 | 调度器需计算剩余移动时间 |
因此ElevatorProcess.state必须是枚举值,且ElevatorScheduler.schedule()中需根据当前状态决定下一步动作:
from enum import Enum class ProcessState(Enum): READY = "READY" RUNNING = "RUNNING" BLOCKED_DOOR_OPEN = "BLOCKED_DOOR_OPEN" BLOCKED_DOOR_CLOSE = "BLOCKED_DOOR_CLOSE" BLOCKED_ACCEL = "BLOCKED_ACCEL" TERMINATED = "TERMINATED" # 在 scheduler.schedule() 中: if self.running_process and self.running_process.state == ProcessState.BLOCKED_DOOR_OPEN: self.running_process.remaining_time -= 1 if self.running_process.remaining_time <= 0: self.running_process.state = ProcessState.BLOCKED_DOOR_CLOSE self.running_process.remaining_time = 1500 # 1.5s in ms2.3.1 参数表:电梯物理行为的关键毫秒级常量
| 参数名 | 含义 | 推荐值(ms) | 说明 |
|---|---|---|---|
DOOR_OPEN_TIME | 开门耗时 | 2000 | 从门开始动到全开 |
DOOR_CLOSE_TIME | 关门耗时 | 1500 | 从门开始关到全闭 |
FLOOR_TRAVEL_TIME | 层间匀速运行时间 | 1200 | 不含加减速,仅中间段 |
ACCEL_DECEL_TIME_PER_FLOOR | 每层加/减速耗时 | 800 | 启动加速或制动减速阶段 |
MAX_WAIT_BEFORE_TIMEOUT | 就绪队列最大等待阈值 | 30000 | 超过 30s 未调度则标记异常 |
这些值不是随意设的,而是参照 GB/T 10058-2009《电梯技术条件》中对客梯的典型参数:额定速度 1.75m/s,层高 3m,加减速度 0.8m/s²,由此反推各阶段时间。
3. 用 60 行核心代码跑通最小可验证调度循环
3.1 调度主循环:时间片驱动,而非事件驱动
电梯调度不是“一有请求就立刻响应”,而是以固定时间片(如 100ms)推进仿真时钟。这才能准确测量等待时间、服务时间、吞吐量。主循环结构如下:
import time from datetime import datetime def run_simulation(scheduler: ElevatorScheduler, duration_ms: int = 60000): start_time = int(time.time() * 1000) current_ms = start_time # 模拟 60 秒运行 while (current_ms - start_time) < duration_ms: # 步骤1:生成新请求(每 5s 随机一个) if (current_ms - start_time) % 5000 < 100: # 每 5s 一次 floor = random.randint(1, 15) target = random.randint(1, 15) while target == floor: target = random.randint(1, 15) req = ElevatorRequest( timestamp=current_ms, source_floor=floor, target_floor=target, direction='UP' if target > floor else 'DOWN' ) scheduler.add_request(req) # 步骤2:调度器执行一个时间片(100ms) scheduler.tick(delta_ms=100) # 步骤3:记录关键指标(每秒打印一次) if (current_ms - start_time) % 1000 < 100: stats = scheduler.get_stats() print(f"[{datetime.now().strftime('%H:%M:%S')}] " f"Queue:{len(scheduler.ready_queue)} " f"Running:{scheduler.running_process.state if scheduler.running_process else 'IDLE'} " f"AvgWait:{stats['avg_wait_ms']:.0f}ms") current_ms += 100 time.sleep(0.1) # 真实延时,便于观察3.1.1scheduler.tick()的四步原子操作
该方法是整个模型的心脏,必须在一个时间片内完成全部状态更新:
- 检查运行中进程是否结束:若
running_process.remaining_time <= 0,将其置为TERMINATED,并清空引用; - 检查就绪队列是否有新进程可调度:若
running_process is None且ready_queue非空,则popleft()取出首个进程,设置其state=RUNNING,初始化remaining_time; - 推进运行中进程的时间:根据其当前
state,扣减对应remaining_time(如BLOCKED_DOOR_OPEN扣 100ms); - 处理电梯物理状态同步:若进程进入
BLOCKED_DOOR_OPEN,则更新scheduler.current_floor为目标楼层;若进入BLOCKED_ACCEL,则按方向更新current_floor(向上+1,向下-1)。
注意:
tick()内不能有time.sleep(),否则会破坏时间片精度;所有延时都在主循环time.sleep(0.1)中统一控制。
3.2 进程创建与就绪队列注入:用工厂函数封装规则
不是所有请求都立即进就绪队列。真实电梯有“服务方向一致性”约束:若电梯正上行且在 5 楼,它不会响应 3 楼的上行请求(因已过站),但会响应 7 楼的上行请求。因此add_request()需调用策略函数判断是否接纳:
def should_accept_request(self, req: ElevatorRequest) -> bool: if self.direction == 'IDLE': return True # 空闲时接所有 elif self.direction == 'UP': return (req.source_floor >= self.current_floor and req.direction == 'UP') or \ (req.target_floor > self.current_floor and req.direction == 'DOWN') # 下行请求但目标在上方,可捎带 else: # DOWN return (req.source_floor <= self.current_floor and req.direction == 'DOWN') or \ (req.target_floor < self.current_floor and req.direction == 'UP')该逻辑直接对应电梯群控系统中的“区域划分+方向锁定”策略,是 FCFS 在物理约束下的必要增强。
4. 验证调度正确性的三个硬指标:必须写进日志并导出 CSV
4.1 每个进程必须记录完整生命周期时间戳
光看控制台打印不够。每个ElevatorProcess实例需在创建、入队、开始执行、开门、关门、终止时,打点记录毫秒级时间戳。最终生成结构化日志:
# 在 ElevatorProcess.__init__() 中 self.timestamps = { 'created': req.timestamp, 'enqueued': None, 'started': None, 'door_opened': None, 'door_closed': None, 'terminated': None } # 在 scheduler.tick() 中更新 if proc.state == ProcessState.RUNNING and proc.timestamps['started'] is None: proc.timestamps['started'] = current_ms if proc.state == ProcessState.BLOCKED_DOOR_OPEN and proc.timestamps['door_opened'] is None: proc.timestamps['door_opened'] = current_ms # ... 其他状态同理4.1.1 关键指标计算公式(必须可复现)
| 指标名 | 计算公式 | 业务含义 |
|---|---|---|
wait_time_ms | timestamps['started'] - timestamps['enqueued'] | 就绪队列等待时长,反映调度公平性 |
service_time_ms | timestamps['terminated'] - timestamps['started'] | 真实服务耗时,含所有阻塞 |
response_time_ms | timestamps['door_opened'] - timestamps['created'] | 用户从按键到门开的总延迟,体验核心指标 |
throughput_per_min | (completed_count / total_duration_minutes) | 每分钟完成请求数,系统吞吐能力 |
4.2 用 pandas 导出分析 CSV,一行命令查瓶颈
将所有进程日志转为 DataFrame,用一行命令定位超时请求:
import pandas as pd def export_stats_to_csv(processes: List[ElevatorProcess], filename: str): records = [] for p in processes: if p.timestamps['terminated']: records.append({ 'pid': id(p), 'created': p.timestamps['created'], 'enqueued': p.timestamps['enqueued'] or 0, 'started': p.timestamps['started'] or 0, 'door_opened': p.timestamps['door_opened'] or 0, 'terminated': p.timestamps['terminated'], 'wait_time_ms': p.wait_time_ms, 'service_time_ms': p.service_time_ms, 'response_time_ms': p.response_time_ms, 'is_timeout': p.response_time_ms > 30000 # 超 30s 标红 }) df = pd.DataFrame(records) df.to_csv(filename, index=False) # 终端执行: # $ python -c "import pandas as pd; df=pd.read_csv('elevator_log.csv'); print(df[df.is_timeout])"4.2.1 常见瓶颈场景及修复指令
| 场景 | 日志特征 | 修复方式 | 命令示例 |
|---|---|---|---|
| 就绪队列积压 | wait_time_ms持续 >5000ms | 缩短DOOR_OPEN_TIME或增加电梯数(改scheduler实例数) | sed -i 's/DOOR_OPEN_TIME = 2000/DOOR_OPEN_TIME = 1500/' elevator.py |
| 响应延迟突增 | response_time_ms在某时间点集体跳升 | 检查该时段是否集中涌入同向请求,启用 LOOK 调度替代 FCFS | git checkout elevator_scheduler_look.py |
| 进程状态卡死 | 多个进程state长期为BLOCKED_DOOR_OPEN | remaining_time未正确扣减,检查tick()中状态转移逻辑 | grep -n 'BLOCKED_DOOR_OPEN' elevator.py |
5. 进阶技巧:用信号量模拟多电梯协同与请求抢占
5.1 单调度器扩展为多实例:用 threading.Event 控制全局状态
真实建筑有不止一部电梯。要模拟两台电梯协同,不能简单起两个ElevatorScheduler实例——它们会竞争同一组请求。必须引入中央协调器:
import threading class MultiElevatorCoordinator: def __init__(self, elevators: List[ElevatorScheduler]): self.elevators = elevators self.request_lock = threading.Lock() self.global_ready_queue = deque() self.coordinator_thread = threading.Thread(target=self._coordinator_loop) self.coordinator_thread.daemon = True self.coordinator_thread.start() def _coordinator_loop(self): while True: # 每 200ms 扫描一次全局队列 time.sleep(0.2) with self.request_lock: if self.global_ready_queue: # 策略:分配给离请求源楼层最近的空闲电梯 req = self.global_ready_queue.popleft() best_elevator = min( self.elevators, key=lambda e: abs(e.current_floor - req.source_floor) ) best_elevator.add_request(req)5.1.1 抢占式调度的轻量实现:用优先队列替换 deque
若需支持“消防员模式”(紧急请求插队),只需将ready_queue从deque换成heapq,并让ElevatorRequest.priority成为堆排序键:
import heapq class PriorityElevatorScheduler(ElevatorScheduler): def __init__(self): super().__init__() self.ready_queue = [] # 改为 list,用 heapq 维护 def add_request(self, req: ElevatorRequest): # priority: 0=紧急, 1=普通, 2=预约 heapq.heappush(self.ready_queue, (req.priority, req.timestamp, ElevatorProcess(req))) def get_next_request(self): if self.ready_queue: _, _, proc = heapq.heappop(self.ready_queue) # 弹出最高优先级 return proc提示:
heapq元组排序规则是依次比对,(0, 1000, proc)一定排在(1, 500, proc)前,确保紧急请求绝对优先。
5.2 用 logging 模块替代 print,支持实时日志流分析
控制台打印无法做聚合分析。将所有调度事件接入logging,并配置RotatingFileHandler:
import logging from logging.handlers import RotatingFileHandler def setup_logger(): logger = logging.getLogger('elevator') logger.setLevel(logging.INFO) handler = RotatingFileHandler( 'elevator_runtime.log', maxBytes=10*1024*1024, # 10MB backupCount=5 ) formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) return logger # 在 scheduler.tick() 中 logger.info(f"PROCESS[{proc.pid}] STARTED at floor {scheduler.current_floor}") logger.debug(f"QUEUE LENGTH: {len(scheduler.ready_queue)}")然后用tail -f elevator_runtime.log | grep 'STARTED' | wc -l实时统计每秒启动进程数,这是压测时最直观的吞吐监控手段。
5.3 验证调度策略有效性的黄金命令:用 awk 统计响应时间分布
不依赖 Python,用 shell 命令快速验证:
# 从日志提取所有 response_time_ms 字段(假设格式:... response_time_ms=12450 ...) awk '{for(i=1;i<=NF;i++) if($i ~ /response_time_ms=/) {split($i,a,"="); print a[2]}}' elevator_runtime.log | \ sort -n | \ awk 'BEGIN{count=0;sum=0;min=999999;max=0} {count++; sum+=$1; if($1<min)min=$1; if($1>max)max=$1} END{print "COUNT:"count,"AVG:"int(sum/count),"MIN:"min,"MAX:"max,"P95:"int(count*0.95)"th"}' | \ column -t -s' '这条命令输出COUNT: 142 AVG: 8420 MIN: 2100 MAX: 42500 P95: 137th,直接告诉你 95% 的请求响应时间在第 137 个样本值以下——这才是生产环境真正关心的 SLO 指标。
本文还有配套的精品资源,点击获取