nimschedule
受 Python schedule 库启发,为 Nim 重新设计的任务调度库。简洁的流式 API,编译期类型安全,零运行时开销,原生线程支持。
01简介
nimschedule 是一个为 Nim 语言设计的轻量级任务调度库,灵感来自 Python 的 schedule 库,但充分利用了 Nim 语言的特性:
流式 API
链式调用,代码即文档
类型安全
编译期检查,零意外
原生性能
编译为 C,无 GC 压力
线程安全
内置锁保护,后台执行
错误处理
回调式异常捕获
多调度器
独立实例,互不干扰
Python schedule 库需要自己写 while True: schedule.run_pending(); time.sleep(1) 循环。nimschedule 内置后台线程执行、错误回调、线程安全锁和多调度器实例,开箱即用。
02安装
方式一:Nimble 安装
# 在项目目录中
nimble install nimschedule
方式二:直接引用源码
将 src/nimschedule/ 目录复制到你的项目中,然后在编译时指定路径:
# 编译时指定路径
nim c --path:src main.nim
编译要求
后台执行功能需要启用线程支持,编译时必须加 --threads:on。推荐使用 ORC 内存管理(Nim 2 默认):
nim c -r --threads:on --path:src your_app.nim
03快速开始
30 秒上手 nimschedule:
import nimschedule
import std/os
# 注册一个每 2 秒运行的任务
schedule.every(2).seconds.perform:
echo "Hello from nimschedule!"
# 主循环
while true:
schedule.runPending()
sleep(1000)
nimschedule 支持三种注册任务的方式,选择你喜欢的风格:
proc job() = echo "working"
schedule.every(10).seconds.perform(job)
schedule.every(10).seconds.perform() do:
echo "working"
schedule.every(10).seconds.perform:
echo "working"
schedule.every(10).seconds.perform(proc() =
echo "working"
)
在 Nim 2.2+ 中,do: 语法是官方推荐写法。代码块语法 (perform:) 会有弃用警告,但仍可使用。
04基本调度
使用 every(N) 设置间隔数量,然后链式调用时间单位:
# 每 10 秒
schedule.every(10).seconds.perform() do:
echo "每10秒"
# 每 5 分钟
schedule.every(5).minutes.perform() do:
echo "每5分钟"
# 每 2 小时
schedule.every(2).hours.perform() do:
echo "每2小时"
# 每 3 天
schedule.every(3).days.perform() do:
echo "每3天"
# 每 2 周
schedule.every(2).weeks.perform() do:
echo "每2周"
单数形式
every() 不传参数时默认为 1,此时可以用单数形式(语义更清晰):
# 等价于 every(1).seconds
schedule.every().second.perform(job)
schedule.every().minute.perform(job)
schedule.every().hour.perform(job)
schedule.every().day.perform(job)
schedule.every().week.perform(job)
| 方法 | 单数 | 复数 | 说明 |
|---|---|---|---|
seconds | .second | .seconds | 秒级调度 |
minutes | .minute | .minutes | 分钟级调度 |
hours | .hour | .hours | 小时级调度 |
days | .day | .days | 天级调度 |
weeks | .week | .weeks | 周级调度 |
05指定时间 at()
at() 方法用于指定任务运行的具体时间点。格式取决于时间单位:
| 时间单位 | 格式 | 示例 | 含义 |
|---|---|---|---|
| 天 | HH:MM 或 HH:MM:SS | "10:30" | 每天 10:30 |
| 小时 | :MM 或 :MM:SS | :30 | 每小时第 30 分钟 |
| 小时 | MM:SS | 15:30 | 每小时第 15 分 30 秒 |
| 分钟 | :SS | :15 | 每分钟第 15 秒 |
| 星期 | HH:MM | "09:00" | 该日 09:00 |
# 每天 10:30 运行
schedule.every().day.at("10:30").perform(job)
# 每天 10:30:00 精确到秒
schedule.every().day.at("10:30:00").perform(job)
# 每小时第 30 分钟运行
schedule.every().hour.at(":30").perform(job)
# 每小时第 15 分 30 秒
schedule.every().hour.at(":15:30").perform(job)
# 每分钟第 15 秒
schedule.every().minute.at(":15").perform(job)
当 every(N) 的 N > 1 时,at() 仍然有效。例如 every(3).days.at("10:30") 表示每 3 天的 10:30 运行。
06星期调度
使用星期方法来设置每周特定日期运行的任务:
# 每周一早上 9:00
schedule.every().monday.at("09:00").perform() do:
echo "周一早会"
# 每周五下午 5:00
schedule.every().friday.at("17:00").perform() do:
echo "周五周报"
# 每 2 周的周三
schedule.every(2).wednesday.perform() do:
echo "双周三任务"
# 每周六和周日(分别注册)
schedule.every().saturday.perform(weekendJob)
schedule.every().sunday.perform(weekendJob)
| 方法 | 对应日期 |
|---|---|
.monday | 周一 |
.tuesday | 周二 |
.wednesday | 周三 |
.thursday | 周四 |
.friday | 周五 |
.saturday | 周六 |
.sunday | 周日 |
07随机间隔 to()
to() 方法设置间隔的上限,任务将在 interval 到 latest 之间随机运行。适用于需要避免任务扎堆的场景。
import std/random
# 初始化随机种子
randomize()
# 每 5-10 秒之间随机运行
schedule.every(5).to(10).seconds.perform() do:
echo "随机执行"
# 每 1-3 分钟之间随机运行
schedule.every(1).to(3).minutes.perform() do:
echo "随机分钟任务"
使用 to() 前需要调用 randomize() 初始化随机种子,否则每次运行结果相同。
08截止时间 until()
until() 方法设置任务的截止时间,到期后任务自动取消。支持三种参数类型:
字符串格式
# 今天 23:59 截止
schedule.every().minute.until("23:59").perform(job)
# 指定日期截止
schedule.every().hour.until("2025-12-31").perform(job)
# 指定日期时间截止
schedule.every().minute.until("2025-12-31 23:59:59").perform(job)
Time 对象
import std/times
# 1 小时后截止
let deadline = getTime() + initDuration(hours = 1)
schedule.every().minute.until(deadline).perform(job)
Duration 对象
# 从现在起 30 秒后截止
schedule.every(5).seconds.until(initDuration(seconds = 30)).perform() do:
echo "30秒内每5秒运行"
"HH:MM" · "HH:MM:SS" · "YYYY-MM-DD" · "YYYY-MM-DD HH:MM" · "YYYY-MM-DD HH:MM:SS"
10取消任务
方式一:通过 cancelJob()
let myJob = schedule.every().second.perform(job)
# 立即从调度器中移除
schedule.cancelJob(myJob)
方式二:通过 job.cancel()
let oneShot = schedule.every().second.perform(job)
# 标记为运行后取消(下次运行后自动移除)
oneShot.cancel()
方式三:清除所有任务
# 清除所有任务
schedule.clear()
# 清除特定标签的任务
schedule.clear("monitor")
11错误处理
nimschedule 内置错误处理机制。当任务抛出异常时,不会中断调度器,而是调用错误回调:
# 设置全局错误处理器
schedule.onError(proc(e: ref Exception) =
echo "任务出错: ", e.msg
# 可以记录日志、发送通知等
)
# 即使这个任务会抛异常,也不会影响其他任务
schedule.every().second.perform() do:
raise newException(ValueError, "模拟错误")
# 这个任务照常运行
schedule.every().second.perform() do:
echo "我正常运行"
Python schedule 库默认不捕获异常,一个任务出错会中断整个调度循环。nimschedule 默认捕获所有异常,确保调度器持续运行。
12后台执行
这是 nimschedule 相比 Python schedule 的重大改进。无需手写 while True 循环,一行代码启动后台调度:
import nimschedule
# 注册任务
schedule.every(1).seconds.perform() do:
echo "后台运行中..."
# 启动后台调度线程(每 500ms 检查一次)
startBackground(500)
# 主线程可以做其他事情
echo "主线程自由运行"
sleep(5000) # 模拟主线程工作
# 停止后台调度
stopBackground()
检查后台状态
if isBackgroundRunning():
echo "后台调度正在运行"
else:
echo "后台调度已停止"
后台线程使用 std/locks 中的 Lock 保护任务列表,确保线程安全。后台线程执行任务时使用 withLock 加锁,主线程的 runPending() 不会与后台线程冲突。
后台执行需要线程支持,编译时必须加 --threads:on 标志。
13多调度器实例
nimschedule 支持创建多个独立的调度器实例,互不干扰:
import nimschedule
import std/locks
# 创建独立调度器
let workSched = Scheduler(jobs: @[])
initLock(workSched.lock)
let homeSched = Scheduler(jobs: @[])
initLock(homeSched.lock)
# 工作调度器
workSched.every().hour.perform() do:
echo "[工作] 每小时检查"
# 家庭调度器
homeSched.every().day.at("20:00").perform() do:
echo "[家庭] 晚上提醒"
# 独立运行
echo workSched.jobCount() # 1
echo homeSched.jobCount() # 1
echo schedule.jobCount() # 0(默认调度器不受影响)
# 各自独立运行
workSched.runPending()
homeSched.runPending()
Python schedule 库使用全局单例,难以隔离不同模块的任务。nimschedule 的每个 Scheduler 实例完全独立。
14Nim 风格直接调用
除了 schedule.xxx 风格,nimschedule 还提供了全局便捷函数,更符合 Nim 的直接调用风格:
import nimschedule
# 不需要 schedule 前缀
discard every(2).seconds.perform(job)
# 直接调用便捷函数
runAll() # 运行所有任务
runPending() # 运行到期任务
echo jobCount() # 任务数量
clear() # 清除所有
clear("tag") # 按标签清除
schedule.every(10).seconds.do(job)
schedule.run_pending()
schedule.clear()
every(10).seconds.perform(job)
runPending()
clear()
15API 参考
Job 方法(链式 API)
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
every(N) | interval: int = 1 | Job | 创建周期任务,N 为间隔数 |
.second(s) | 无 | Job | 设置单位为秒 |
.minute(s) | 无 | Job | 设置单位为分钟 |
.hour(s) | 无 | Job | 设置单位为小时 |
.day(s) | 无 | Job | 设置单位为天 |
.week(s) | 无 | Job | 设置单位为周 |
.monday~.sunday | 无 | Job | 设置星期调度 |
.at(timeStr) | timeStr: string | Job | 指定运行时间 |
.to(latest) | latest: int | Job | 设置随机间隔上限 |
.until(...) | string | Time | Duration | Job | 设置截止时间(3 种重载) |
.tag(tags) | varargs[string] | Job | 添加标签 |
.perform(action) | JobAction | Job | 注册并启动任务 |
.cancel() | 无 | 无 | 标记运行后取消 |
Scheduler 方法
| 方法 | 参数 | 返回 | 说明 |
|---|---|---|---|
runPending() | 无 | 无 | 运行所有到期任务 |
runAll(delay) | delaySeconds: float = 0 | 无 | 立即运行所有任务 |
getJobs(tag) | tag: string = "" | seq[Job] | 获取任务列表 |
clear(tag) | tag: string = "" | 无 | 清除任务 |
cancelJob(job) | job: Job | 无 | 取消指定任务 |
nextRun(tag) | tag: string = "" | Option[Time] | 下次运行时间 |
idleSeconds() | 无 | Option[float] | 距下次运行秒数 |
jobCount() | 无 | int | 当前任务数 |
onError(handler) | auto | 无 | 设置错误回调 |
startBackground(ms) | intervalMs: int = 1000 | 无 | 启动后台线程 |
stopBackground() | 无 | 无 | 停止后台线程 |
类型定义
type
TimeUnit* = enum
tuNone, tuSecond, tuMinute, tuHour, tuDay, tuWeek,
tuMonday, tuTuesday, tuWednesday, tuThursday,
tuFriday, tuSaturday, tuSunday
JobAction* = proc() {.closure.}
Job* = ref object
interval*: int
latest*: int
unit*: TimeUnit
atTime*: string
tags*: HashSet[string]
action*: JobAction
nextRun*: Time
lastRun*: Time
untilTime*: Option[Time]
cancelAfterRun*: bool
Scheduler* = ref object
jobs*: seq[Job]
onErrorCb*: ErrorHandler
bgRunning*: bool
bgThread*: Thread[void]
lock*: Lock
16与 Python schedule 对比
import schedule
import time
def job():
print("working")
schedule.every(10).seconds.do(job)
schedule.every().day.at("10:30").do(job)
while True:
schedule.run_pending()
time.sleep(1)
import nimschedule
schedule.every(10).seconds.perform() do:
echo "working"
schedule.every().day.at("10:30").perform() do:
echo "daily"
# 无需 while 循环!
startBackground(1000)
功能对比
| 功能 | Python schedule | nimschedule |
|---|---|---|
| 基本调度 | every(N).seconds.do(fn) | every(N).seconds.perform(fn) |
| 指定时间 | .at("10:30") | .at("10:30") |
| 星期调度 | .monday.do(fn) | .monday.perform(fn) |
| 随机间隔 | .to(10).do(fn) | .to(10).perform(fn) |
| 截止时间 | .until("time").do(fn) | .until("time").perform(fn) |
| 标签管理 | .tag("name").do(fn) | .tag("name").perform(fn) |
| 错误处理 | 需自行 try/except | 内置 onError 回调 |
| 后台执行 | 需手写 while 循环 | 内置 startBackground() |
| 线程安全 | 无 | 内置 Lock 保护 |
| 多调度器 | 需 Scheduler() 类 | 原生 Scheduler 实例 |
| 类型安全 | 运行时 | 编译期 |
| 性能 | 解释执行 | 编译为原生代码 |
17Nim 优势
编译期类型检查
Nim 的强类型系统在编译期捕获类型错误。JobAction 类型确保只能传递无参数闭包,避免运行时崩溃。
零开销抽象
链式 API 编译后等价于直接函数调用。{.inline.} 标记的方法在编译时内联,无额外开销。
ORC 内存管理
Nim 2 的 ORC 内存管理器自动处理引用计数,无需手动管理内存,也无需 GC 暂停。
原生线程支持
std/threads 提供原生线程,后台调度在独立线程运行,不阻塞主线程。锁保护确保线程安全。
模板元编程
perform 的代码块语法通过 Nim 模板实现,编译时展开为闭包,无运行时开销。
单文件部署
Nim 编译为单个原生可执行文件,无需运行时依赖。调度器可以直接嵌入任何 Nim 项目。
性能对比
| 指标 | Python schedule | nimschedule |
|---|---|---|
| 执行方式 | 解释执行 (CPython) | 编译为 C 原生代码 |
| 内存管理 | GC (引用计数+分代) | ORC (引用计数) |
| 任务调度开销 | ~微秒级 | ~纳秒级 |
| 启动时间 | ~100ms | ~1ms |
| 二进制大小 | 需 Python 运行时 | 单文件 ~100KB |
| 依赖 | Python 3.x | 无 (静态链接) |
nimschedule 在保持 Python schedule 库简洁 API 的同时,通过 Nim 语言的特性获得了编译期类型安全、原生性能、内置后台执行和线程安全等优势。它是嵌入式系统、CLI 工具、服务端应用的理想调度方案。