文章目录
-
- 前言
- [1. 先说清楚:任务调度是个啥](#1. 先说清楚:任务调度是个啥)
-
- [1.1 定时器:不用你盯着的闹钟](#1.1 定时器:不用你盯着的闹钟)
- [1.2 一个任务一个定时器?劝你冷静](#1.2 一个任务一个定时器?劝你冷静)
- [1.3 任务调度器 = 总包工头](#1.3 任务调度器 = 总包工头)
- [2. 第一步:给任务办张"员工卡"(Task 类)](#2. 第一步:给任务办张"员工卡"(Task 类))
-
- [2.1 员工卡上写什么](#2.1 员工卡上写什么)
- [2.2 暂停和恢复:就改一个数](#2.2 暂停和恢复:就改一个数)
- [3. 第二步:请个包工头(Scheduler 类)](#3. 第二步:请个包工头(Scheduler 类))
-
- [3.1 开工前的初始化](#3.1 开工前的初始化)
- [3.2 定时器中断:查岗铃声](#3.2 定时器中断:查岗铃声)
- [3.3 干活:_run 的食堂逻辑](#3.3 干活:_run 的食堂逻辑)
- [3.4 主循环:永动机](#3.4 主循环:永动机)
- [3.5 增删改查全家桶](#3.5 增删改查全家桶)
- [4. 第三步:上板实测](#4. 第三步:上板实测)
-
- [4.1 准备工作](#4.1 准备工作)
- [4.2 掐表神器:计时装饰器](#4.2 掐表神器:计时装饰器)
- [4.3 任务回调:职场连续剧](#4.3 任务回调:职场连续剧)
- [4.4 保洁阿姨和复读机](#4.4 保洁阿姨和复读机)
- [4.5 初始化与开跑](#4.5 初始化与开跑)
- [4.6 终端输出赏析](#4.6 终端输出赏析)

P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。
前言
兄弟们,先问个直击灵魂的问题:你的主循环里,有没有那种"while True 里塞了七八个 if,每个 if 都在掐着时间干活"的操作?有的话,恭喜你,你已经是一个成熟的"人肉调度器"了。今天不聊虚的,直接上树莓派 Pico,用 MicroPython 的定时器,手搓一个正儿八经的任务调度器。
1. 先说清楚:任务调度是个啥
1.1 定时器:不用你盯着的闹钟
先想个场景:你的程序里有一堆"每隔一会儿就得干一次"的活------读个传感器、刷新个屏幕、记个日志。全塞进主循环里用时间硬判断?可以,但代码写完基本就是一场灾难现场,你自己都懒得看第二遍。
定时器就是来救命的。说穿了,它就是个不用你盯着的闹钟:你告诉它"500 毫秒响一次",它就真的一次不差地响。比某些人的自律靠谱多了。
MicroPython 里现成的闹钟库是 machine.Timer:创建定时器、单次或周期运行、执行回调、停止,一条龙全包。省心程度堪比外卖送到家门口。
1.2 一个任务一个定时器?劝你冷静
有人可能一拍大腿:任务多?多开几个定时器不就完了。听起来很美,但咱举个现实点的需求:
- 任务 1:每 500ms 执行一次,程序跑满 3000ms 后光荣退役;
- 任务 2:每 1000ms 执行一次,3000ms 时暂停,7000ms 时满血复活;
- 任务 3:程序跑到 3000ms 之后,每 1000ms 上一次班;
- 全员没事干的时候,还要记得扫地(垃圾回收)。
你要是用一堆定时器去实现这玩意儿,全局变量多到能开超市,判断条件叠起来比双十一的优惠券还复杂。更崩溃的是任务之间还有联动:任务 1 干完活可能得把任务 3 叫起来,任务 2 跑到一半还得改自己的周期。这时候每个定时器都是一个独立的小作坊,彼此全靠全局变量传话------改一个任务,等于给全身做一次大手术。
记住:定时器开得越多,你的头发掉得越快。这不是玄学,是全局变量的诅咒。
1.3 任务调度器 = 总包工头
所以正确的姿势是:一个定时器 + 一个任务调度器。
任务调度器是什么?说穿了就是总包工头:自己不干活,专门安排谁干活。像极了组里那个不写代码只排期的组长。
它主要干两件事:
- 任务管理:给每个任务建档(回调函数、执行周期、参数),提供添加、删除、暂停、恢复的入口,维护一份在编人员名单;
- 任务调度:定期扫一眼名单,谁的班到了就喊谁上岗,谁在休假就跳过谁,出了岔子还有兜底方案。
实现思路也朴素得感人:一个定时器定期"叮"一下,调度器就扫一遍任务列表,该干的干,不该干的继续歇着。就这么简单,别想复杂了。
2. 第一步:给任务办张"员工卡"(Task 类)
2.1 员工卡上写什么
先造一个 Task 类。你可以把它理解成入职登记表:多久上一次班(interval)、上班要干点啥(callback)、带不带家伙什儿(param)、现在是上班还是摸鱼(state)。
代码放这儿了,写在 Scheduler.py 文件里:
python
# ======================================== 导入相关模块 ========================================
from micropython import const # 导入const常量标识符
from machine import Timer # 导入硬件相关模块
# ======================================== 自定义类 ============================================
# 定义Task任务类
class Task():
"""Task类:一个定时执行的任务,含回调函数、参数、执行间隔和任务状态。"""
# 任务状态标识符
TASK_RUN = const(0) # 任务运行状态
TASK_STOP = const(1) # 任务停止状态
def __init__(self, callback: callable, *param: object,
interval: int = 1000, state: int = TASK_RUN) -> None:
self._callback = callback # 任务执行时调用的回调函数
self._param = param # 传递给回调函数的参数
self._intv = interval # 任务执行的时间间隔(ms)
self._state = state # 任务状态
self._cnt = 10 # 任务计数器(默认值,添加任务时会重算)
self._rt = 0 # 任务计数/辅助信息
def pause(self) -> None:
"""暂停任务:状态置为TASK_STOP"""
self._state = Task.TASK_STOP
def resume(self) -> None:
"""恢复任务:状态置为TASK_RUN"""
self._state = Task.TASK_RUN
def run(self) -> None:
"""执行任务回调函数,并传入相关参数"""
self._callback(*self._param)
看到没,状态就俩:TASK_RUN(0)和 TASK_STOP(1)。比某些公司的"996/007"制度简单多了。
2.2 暂停和恢复:就改一个数
接下来是暂停和恢复。你猜实现有多复杂?
python
def pause(self) -> None:
self._state = Task.TASK_STOP
def resume(self) -> None:
self._state = Task.TASK_RUN
就改一个状态值。暂停一个任务,比暂停你正在追的剧还简单------连进度条都不用记。
原理也很朴实:调度器扫名单的时候,看到状态是 STOP 就直接跳过,看到 RUN 才喊人干活。至于 run(),更朴实:拿上你的参数,把回调函数喊起来干活。毫无技巧,全是感情。
3. 第二步:请个包工头(Scheduler 类)
3.1 开工前的初始化
python
# 定义Scheduler调度类
class Scheduler():
"""Scheduler类:通过定时器周期性调度任务,支持增删、暂停、恢复和执行。"""
def __init__(self, tm: Timer, interval: int = 100,
task_idle: callable = None, task_err: callable = None) -> None:
self._tasks = [] # 任务列表
self._task_idle = task_idle # 任务空闲回调函数
self._task_err = task_err # 任务错误回调函数
self._interval = interval # 定时器间隔(ms)
self._tmr = tm # 定时器实例
self._tmr.init(period=interval, callback=self._tmrirq) # 初始化定时器
初始化要传四样东西:一个定时器实例(总闹钟)、查岗间隔(默认 100ms)、空闲回调(没人干活时干啥)和错误回调(出事找谁)。最后那一行 _tmr.init(period=interval, callback=self._tmrirq),就是把闹钟设置好,到点自动响铃。
3.2 定时器中断:查岗铃声
每次定时器一响,就会触发 _tmrirq()。它干的事特别简单:把任务列表从头到尾扫一遍,凡是状态为 RUN 的,工龄(_rt)加一。
python
def _tmrirq(self, t: Timer) -> None:
"""定时器中断回调:遍历任务列表,为运行中的任务计数"""
for i in range(len(self._tasks)):
if self._tasks[i]._state == Task.TASK_RUN:
self._tasks[i]._rt += 1 # 任务执行时间间隔 +1
注意,它只负责"记考勤",不负责"安排活"。真正的活儿在下头。
3.3 干活:_run 的食堂逻辑
_run() 的逻辑,我愿称之为"食堂打饭":
- 状态不是 RUN?对不起,窗口没开,排队去吧;
- 工龄还没攒够(
_rt小于_cnt)?继续老实等着; - 到点了(
_rt >= _cnt)?打饭!执行回调,然后把工龄清零重新攒; - 万一饭里有虫子(异常)?喊食堂主任(
task_err)来处理。
python
def _run(self, task: Task) -> None:
"""执行单个任务回调函数,并处理异常"""
if task._state == Task.TASK_RUN:
try:
if task._rt >= task._cnt: # 到达任务执行间隔
task._rt = 0
task.run()
except Exception as e:
if self._task_err: # 有异常回调就交给它处理
self._task_err(e)
3.4 主循环:永动机
scheduler() 是个死循环,堪称嵌入式界的永动机:只要你不按 Ctrl+C,它就能一直转。比健身房年卡还持久。
python
def scheduler(self) -> None:
"""调度器主循环:轮询检测任务状态,判断是否执行任务"""
while True:
try:
for i in range(len(self._tasks)):
task = self._tasks[i]
self._run(task)
if self._task_idle: # 空闲时执行任务空闲回调
self._task_idle()
except KeyboardInterrupt:
return
except Exception as e:
print('except {}'.format(e))
循环里就干三件事:挨个检查任务该不该执行、空闲了喊保洁(task_idle)、出异常打印一句继续转。主打一个皮实耐造。
3.5 增删改查全家桶
剩下的方法放一块儿看,都是给包工头用的管理工具:
python
def find(self, task: Task) -> int:
"""查找任务在列表中的索引"""
try:
return self._tasks.index(task)
except:
return None
def clear(self) -> None:
"""清空任务列表"""
self._tasks.clear()
def add(self, task: Task, state: int = Task.TASK_RUN) -> None:
"""添加任务到任务列表"""
if self.find(task) == None:
self._tasks.append(task)
# 任务需要执行的时间间隔 = 任务间隔 // 定时器间隔
task._cnt = task._intv // self._interval
print('add task:', task._callback.__name__)
if state == Task.TASK_STOP:
self.pause(task)
def delete(self, task: Task) -> None:
"""从任务列表删除指定任务"""
try:
self._tasks.remove(task)
except:
print('del task <', task, '> error')
def pause(self, task: Task) -> None:
"""暂停指定任务"""
if self.find(task) != None:
self._tasks[self.find(task)].pause()
def resume(self, task: Task) -> None:
"""恢复指定任务"""
if self.find(task) != None:
self._tasks[self.find(task)].resume()
def run(self, task: Task) -> None:
"""立即执行指定任务"""
if self.find(task) != None:
task._rt = task._cnt
self._run(task)
add():新人入职。先查重,不在名单里才收,顺手算一下"上班周期换算"(任务间隔 ÷ 查岗间隔),再喊一嗓子"add task: xxx";delete():办理离职。删不到人还会报个 error,像极了 HR 想辞人又找不到流程;pause()/resume():放假和销假,本质就是改员工卡上的状态;run():特殊通道,直接让某个任务插队,立即执行;find()/clear():查员工号、清空整家公司。
到这里,调度器本体就完工了。别急,光有包工头和员工卡还不够,得拉出去上板遛遛。
4. 第三步:上板实测
4.1 准备工作
接下来在 main.py 里写示例程序。先导入一条龙:
python
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @File : main.py
# @Description : 定时器类实验,使用定时器完成任务调度
# ======================================== 导入相关模块 ========================================
from machine import Timer # 导入硬件相关的模块
from Scheduler import Scheduler, Task # 导入任务调度器
import time # 导入时间模块
import gc # 垃圾回收的模块
import sys # 导入系统相关的模块
# ======================================== 全局变量 ============================================
time_start = time.ticks_us() # 任务开始时间
RunCnt = 0 # 方法执行次数
4.2 掐表神器:计时装饰器
全局变量就俩:time_start 记录开跑时间,RunCnt 数任务跑了多少次。接下来是全场最有仪式感的工具------计时装饰器:
python
# 计时装饰器,用于计算函数运行时间
def timed_function(f: callable, *args: tuple, **kwargs: dict) -> callable:
"""计时装饰器:计算并打印函数/方法运行时间"""
myname = str(f).split(' ')[1]
def new_func(*args: tuple, **kwargs: dict) -> any:
t: int = time.ticks_us()
result = f(*args, **kwargs)
delta: int = time.ticks_diff(time.ticks_us(), t)
print('Function {} Time = {:6.3f}ms'.format(myname, delta / 1000))
return result
return new_func
每次任务跑完都报个耗时,精确到小数点后三位。这种掐表仪式感,建议全公司推广------以后谁再说自己代码性能好,先跑个 1ms 以内的再说话。
4.3 任务回调:职场连续剧
python
# 使用@timed_function装饰器,计算任务运行时间
@timed_function
def task_callback(task_id: int) -> None:
"""任务回调函数:打印任务信息并管理任务状态"""
global time_start, RunCnt, sc, task1, task2
# 计算从起始时间到现在的时间差(ms)
time_now = (time.ticks_us() - time_start) / 1000
# 输出任务运行次数、运行时间和任务ID
print('{} - {:.2f} ms: task {} is running'.format(RunCnt, time_now, task_id))
# 任务运行次数加1
RunCnt = RunCnt + 1
# 第5次运行:暂停任务2、创建任务3、删除任务1
if RunCnt == 5:
sc.pause(task2)
print('pause task 2')
task3 = Task(task_callback, 3, interval=1000, state=Task.TASK_RUN)
sc.add(task3)
print('add task 3')
sc.delete(task1)
print('delete task 1')
# 第8次运行:恢复任务2
if RunCnt == 8:
sc.resume(task2)
task_callback 是这次实验的主角。它每次干活就三件事:算时间、打印"第几次 + 哪个任务在跑"、计数加一。然后在两个关键节点搞事情:
- 第 5 次:暂停任务 2、创建任务 3 并办入职、把任务 1 开除;
- 第 8 次:把任务 2 的假销了,恢复上岗。
一套连招下来,任务 1 直接失业,任务 3 空降入职,任务 2 原地待命------活脱脱一部职场连续剧,还是带裁员的那种。
4.4 保洁阿姨和复读机
python
# 空闲任务回调函数
def task_idle_callback() -> None:
"""空闲回调:内存不足时手动触发垃圾回收"""
if gc.mem_free() < 230000: # 可用堆RAM小于230000字节时
gc.collect() # 手动触发垃圾回收
# 异常回调函数
def task_err_callback(e: Exception) -> None:
"""异常回调:打印异常信息并处理任务错误"""
while True:
sys.print_exception(e)
print('task run error')
task_idle_callback 是保洁阿姨:一看到内存快见底(剩余堆 RAM 小于 230000 字节),立刻抄起 gc.collect() 开始扫地,勤快到感人。
task_err_callback 更狠,是个复读机:一出异常,无限循环打印 "task run error"。就这个执着劲儿,建议拿去治疗拖延症,比闹钟好使。
4.5 初始化与开跑
python
# ======================================== 初始化配置 ==========================================
# 上电延时3s
time.sleep(3)
print("FreakStudio : Using Timer to implement a simple task scheduler")
# 创建任务1,500ms执行一次
task1 = Task(task_callback, 1, interval=500, state=Task.TASK_RUN)
# 创建任务2,1000ms执行一次
task2 = Task(task_callback, 2, interval=1000, state=Task.TASK_RUN)
# 创建任务调度器,定时周期为100ms
sc = Scheduler(Timer(-1), interval=100,
task_idle=task_idle_callback, task_err=task_err_callback)
# 添加任务
sc.add(task1)
sc.add(task2)
# ======================================== 主程序 ============================================
# 延时2s,等待烧录完成程序后打开终端
time.sleep(2)
# 开启调度
sc.scheduler()
任务 1 每 500ms 上班一次,任务 2 每 1000ms 一次,调度器每 100ms 查一次岗。先睡 3 秒热身、再睡 2 秒等终端,然后 sc.scheduler() 正式开跑。这套"先睡再干"的流程,跟周一早上的你一模一样。
4.6 终端输出赏析
烧录完代码,打开终端,你会看到一场大戏缓缓上演:
0 - 2002.68 ms: task 1 is running
Function task_callback Time = 1.435ms
1 - 2004.84 ms: task 2 is running
Function task_callback Time = 0.944ms
2 - 2505.53 ms: task 1 is running
Function task_callback Time = 1.082ms
3 - 3006.26 ms: task 1 is running
Function task_callback Time = 1.058ms
4 - 3008.06 ms: task 2 is running
pause task 2
add task: new_func
add task 3
delete task 1
5 - 4008.66 ms: task 3 is running
Function task_callback Time = 1.101ms
6 - 5008.12 ms: task 3 is running
Function task_callback Time = 1.063ms
7 - 6007.49 ms: task 3 is running
Function task_callback Time = 1.315ms
8 - 7007.90 ms: task 2 is running
Function task_callback Time = 1.096ms
9 - 7009.72 ms: task 3 is running
...(任务2和任务3轮流上岗,循环往复)
来,对照着看这场戏:
- 任务 1 每 500ms 准时蹦一次,任务 2 每 1000ms 蹦一次,比上班打卡还准;
- 第 5 次运行后,任务 2 被暂停(pause task 2)、任务 3 入职(add task 3)、任务 1 被删除(delete task 1),动作一气呵成;
- 第 8 次运行后,任务 2 满血复活,跟任务 3 开始轮流上岗,哥俩好。
而且你注意,每个回调后面都跟着毫秒级耗时,最快的不到 1ms。就这个速度,你甚至可以在任务里再塞几个任务------只要别把自己塞死机就行。
最后总结一下:这一套"一个定时器 + 一个调度器"的方案,结构清爽、扩展方便,任务增删、暂停、恢复全都有现成接口,比你堆一堆全局变量强一万倍。下次再有人说"嵌入式做不了多任务",你就把这个调度器拍他脸上。
当然,如果你的任务真的复杂到需要跑操作系统,那当我没说------毕竟这玩意儿再牛,也只是个业余版 RTOS,专业选手请绕道。
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。