一、最常见的误解:sleep 并不会精准等待
很多人以为:
await asyncio.sleep(1)
一定会在 1 秒后恢复执行。
实际上:
它只保证“至少等待 1 秒”。
真正恢复执行的时间,取决于:
事件循环是否空闲
当前任务数量
系统调度
CPU 忙碌程度
selector/poll 精度
操作系统时钟粒度
因此:
import asyncio
import time
async def test():
start = time.time()
await asyncio.sleep(1)
print(time.time() - start)
asyncio.run(test())
输出可能是:
1.0012
1.0087
1.0153
甚至更高。
二、小时间 sleep 误差会更明显
例如:
await asyncio.sleep(0.001)
理论是 1ms。
但真实情况可能是:
0.005
0.01
0.015
原因是:
很多操作系统本身的调度粒度就没那么高。
Windows 默认时间片通常约:
15.6ms
所以你即使 sleep 1ms:
系统也可能 15ms 后才恢复。
Linux 通常会好一些。
三、sleep(0) 并不是真的“0等待”
很多人用:
await asyncio.sleep(0)
实现“主动让出控制权”。
这确实是 asyncio 官方推荐写法。
但它:
不代表立即恢复。
它只是:
把当前协程重新放回事件循环队列尾部
例如:
async def task():
while True:
print("A")
await asyncio.sleep(0)
如果还有大量任务:
A
A
A
之间的时间间隔可能并不稳定。
四、大量协程会放大时间漂移
看下面:
import asyncio
import time
async def worker():
while True:
print(time.time())
await asyncio.sleep(1)
asyncio.run(worker())
很多人以为会:
00.000
01.000
02.000
03.000
但真实可能:
00.000
01.003
02.007
03.012
04.018
误差会越来越大。
因为:
sleep(1)
→ 唤醒
→ 执行代码
→ 再 sleep(1)
执行代码本身也消耗时间。
五、正确做法:使用“绝对时间”
高精度定时任务通常这样写:
import asyncio
import time
async def timer():
interval = 1
next_time = time.monotonic()
while True:
print(time.time())
next_time += interval
await asyncio.sleep(
max(0, next_time - time.monotonic())
)
asyncio.run(timer())
这样误差不会不断累计。
因为:
每次都基于目标时间计算
而不是:
执行完后再等待1秒
六、事件循环本身也影响精度
不同事件循环实现差异很大。
例如:
Linux 默认 loop
通常基于:
epoll
精度较高。
Windows 默认 loop
Python 3.8 后:
ProactorEventLoop
但时间调度仍受 Windows 时钟影响。
uvloop
很多人换:
uvloop
后发现:
定时更稳定
延迟更低
吞吐更高
因为它底层基于:
libuv
调度效率更强。
七、sleep 精度会影响限速器
例如爬虫:
await asyncio.sleep(0.1)
理论:
每秒10次
但实际上:
可能 8 次
可能 12 次
因为:
sleep误差 + 调度延迟
所以很多限速器都会:
用 token bucket
用 monotonic
用绝对时间
而不是简单 sleep。
八、sleep 精度问题会导致“协程雪崩”
一种经典问题:
while True:
await asyncio.sleep(60)
await do_job()
如果:
do_job()
执行 5 秒。
那么:
实际周期 = 65 秒
运行一天后:
任务时间会严重漂移。
很多定时系统最终会:
全部错峰
时间错乱
任务堆积
九、为什么 asyncio 不追求绝对精准
因为 asyncio 核心目标是:
高并发 IO 调度
不是:
实时系统
它不适合:
高频交易
精密实时控制
毫秒级工业控制
音频实时处理
这些通常需要:
RTOS
C/C++
内核级定时器
实时调度器
十、一个非常隐蔽的问题:时钟回拨
如果你用:
time.time()
计算 sleep。
系统时间被修改后:
NTP 校时
手动改时间
虚拟机同步
可能导致:
sleep时间异常
因此 asyncio 内部大量使用:
time.monotonic()
它不会受系统时间变化影响。
十一、实战建议
普通业务
直接:
await asyncio.sleep()
即可。
定时任务
使用:
time.monotonic()
做绝对时间调度。
高并发限速
不要依赖 sleep 精度。
使用:
令牌桶
时间窗口
漏桶算法
高精度场景
考虑:
uvloop
trio
Rust Tokio
Go runtime
总结
asyncio.sleep() 最大的问题不是“不准”。
而是:
误差会累计
尤其在:
长时间运行
大量协程
高频任务
定时系统
中。
很多线上系统的“越来越慢”“定时漂移”“任务堆积”,根源其实就是:
把 asyncio.sleep 当成了精准定时器