心跳检测不能用 time.sleep() 因其会阻塞事件循环,必须用 asyncio.sleep() 并通过 asyncio.create_task() 启动永续轮询协程,配合超时控制、连接池复用、信号量限流及 dns 缓存优化。

心跳检测为什么不能用 time.sleep() 阻塞式轮询
因为会卡死整个事件循环,其他协程无法执行。比如你用 asyncio.sleep() 替代,才能让出控制权;而直接写 time.sleep(5) 在 async def 里,等于把整个 asyncio.run() 线程挂起 5 秒——这不是异步,是“假装异步”。
常见错误现象:RuntimeWarning: coroutine 'check_heartbeat' was never awaited,往往是因为忘了 await 或误用了同步函数。
- 必须用
asyncio.sleep(),不是time.sleep() - 心跳任务得通过
asyncio.create_task()启动,不能直接调用协程对象 - 如果目标服务响应慢,要设
asyncio.wait_for()超时,否则单次失败会拖垮整个轮询节奏
如何用 asyncio.create_task() 启动长期运行的心跳协程
核心是别让它被主协程“等完就结束”——得让它自己跑着,同时不阻塞别的逻辑。典型结构是用 while True + await asyncio.sleep() 构成永续轮询,再用 asyncio.create_task() 扔进事件循环。
示例片段:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
async def heartbeat_worker(url: str, interval: float = 10.0):
while True:
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=3.0) as resp:
if resp.status != 200:
print(f"⚠️ {url} returned {resp.status}")
except Exception as e:
print(f"❌ {url} failed: {e}")
await asyncio.sleep(interval)
<h1>启动(不是 await!)</h1><p>asyncio.create_task(heartbeat_worker("<a href="https://www.php.cn/link/1b4ea86404d0b479c4a73e55dfd8b0b7">https://www.php.cn/link/1b4ea86404d0b479c4a73e55dfd8b0b7</a>"))</p>
- 不要在主
async def main()里await heartbeat_worker(...),否则主流程卡住 - 异常必须捕获,否则一次网络错误会让整个 task 崩溃退出
- 每个
ClientSession应该复用,而不是每次新建——但注意它不能跨async with生命周期,推荐提取为全局 session 或用依赖注入
aiohttp 和 httpx 在心跳场景下的关键差异
两者都支持异步 HTTP,但默认行为不同,直接影响心跳稳定性:
-
aiohttp的ClientSession默认不复用连接池,需显式传connector=aiohttp.TCPConnector(limit=10),否则高频轮询可能耗尽本地端口 -
httpx.AsyncClient默认启用连接池和 HTTP/2 支持(服务端配合时),且超时设置更直观:timeout=httpx.Timeout(3.0, connect=2.0) - 若目标服务只支持 HTTP/1.1,
httpx在某些版本中会因默认启用 HTTP/2 探测而多一次往返,可加http2=False关闭 - 错误类型不同:
aiohttp抛aiohttp.ClientError,httpx抛httpx.RequestError,统一处理时建议按基类捕获
如何避免多个心跳任务互相干扰或资源挤占
当监控十几个 endpoint 时,全开独立 task 容易触发连接数上限、DNS 查询风暴或日志刷屏。需要做轻量节制:
- 用
asyncio.Semaphore控制并发请求数,例如限制最多 5 个心跳同时发出去:sem = asyncio.Semaphore(5),然后在请求前async with sem: - DNS 缓存很重要:
aiohttp.TCPConnector(resolve_timeout=2.0, use_dns_cache=True),否则每次get()都查 DNS - 日志别用
print(),改用logging.getLogger(__name__).info(),并配置level=logging.WARNING过滤掉正常心跳成功日志 - 心跳间隔不宜过短(如
真正难的不是启动几个协程,而是让它们长期稳住不泄漏连接、不累积异常、不把监控本身变成故障源。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










