基于定时器的手搓任务调度器:树莓派Pico实战

文章目录

    • 前言
    • [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的朋友,否则看看零散的博文就够了。

相关推荐
程序员cxuan1 小时前
ChatGPT 开启无限 token
人工智能·后端·程序员
机核研创社1 小时前
HN 慧拿 HN-06 全自动上橡筋机|圆筒扩张结构与三针五线绷缝原理
人工智能·自动化
AI增长技术研究院1 小时前
品牌官网没有被AI引用,怎样区分页面发现问题与内容适用问题?
人工智能
xcLeigh1 小时前
AI写作的前世今生:从规则模板到大语言模型的演进之路
人工智能·ai·ai写作
前端.火鸡1 小时前
AI取代互联网开发者:冲击、优势与行业重构的深度分析
人工智能
DogDaoDao1 小时前
HYPERmotion 深度解析:当人形机器人学会“自己想招“——LLM 规划 + RL 技能库 + 全身优化的混合行为规划框架
人工智能·机器人·人形机器人·运动轨迹·自由度·运动估计·hypermotion
常山云栈1 小时前
axios和restful的区别是什么?
人工智能
wshzd1 小时前
LLM之Agent(六十九)|PI(八)发布流程与供应链安全
人工智能
罗西的思考1 小时前
[Agent Memory / 强化学习] MemPO源码学习笔记 — (2)— 训练
人工智能·深度学习