Python Pystray开发中可否使用asyncio替代threading实现定时更新?
方案选型判断
直接回应核心疑问:
对
pystray.Icon.run()的调用为阻塞式调用,且必须从应用的主线程发起,原因是OSX平台的系统托盘图标实现要求必须从主线程调用才能正常运行,同时该平台要求应用运行循环处于运行状态,pystray.Icon.run()会自动启动该运行循环。
- 上述官方要求仅约束托盘图标的启动调用位置,完全没有禁止asyncio的使用,更不意味着asyncio和该场景不兼容
- 你依然需要把异步逻辑放到独立后台线程运行,但这个线程跑的是asyncio事件循环,而非原来的同步
time.sleep循环 - 对于爬取这类IO密集型逻辑,asyncio方案比现有同步threading方案更有优势:不需要为每个阻塞的网络请求单独占一个线程,单线程就能并发处理多个爬取请求,资源占用更低,也不会因为同步请求阻塞导致定时任务漂移
- 绝对不能把asyncio的主事件循环放到主线程运行,否则会抢占主线程的系统托盘运行循环,在OSX平台上会直接出现图标无响应、点击无反馈的兼容问题
可直接落地的asyncio版本实现
这个实现完全遵循官方的线程要求,和你现有版本的行为逻辑一致,支持60秒间隔的定时更新,同时保留手动点击更新的菜单功能。
from pystray import Icon as icon, Menu as menu, MenuItem as item from PIL import Image, ImageDraw, ImageFont import asyncio import random import threading def make_number_icon(number): # 在透明背景上绘制红色两位数字 img = Image.new('RGBA', (64,64), color=(0,0,0,0)) fnt = ImageFont.truetype(r'C:\Windows\Fonts\GOTHICB.TTF', 58) d = ImageDraw.Draw(img) d.text((0,-8), f"{number:02}", font=fnt, fill='red') return img # 全局共享的托盘icon实例 tray_icon = None async def fetch_latest_number(): # 替换为实际异步爬取逻辑,用随机数做演示 await asyncio.sleep(1) # 模拟网络请求耗时 return random.randrange(100) async def update_icon(): count = await fetch_latest_number() # 更新托盘图标,pystray支持从后台线程修改icon属性 global tray_icon tray_icon.icon = make_number_icon(count) async def async_update_loop(interval: int = 60): # 以执行完成时间为基准,按固定间隔触发更新 while True: await update_icon() await asyncio.sleep(interval) def start_async_loop(loop, update_interval): # 在后台线程启动asyncio事件循环 asyncio.set_event_loop(loop) loop.run_until_complete(async_update_loop(update_interval)) if __name__ == "__main__": # 创建独立的asyncio事件循环 async_loop = asyncio.new_event_loop() # 初始化托盘图标,把手动更新的回调提交到后台异步循环执行 tray_icon = icon( "Pystray test", icon=make_number_icon(0), menu=menu( item("Update now", lambda i: asyncio.run_coroutine_threadsafe(update_icon(), async_loop)), item("Quit", tray_icon.stop) ) ) # 把异步循环放到daemon线程运行,主程序退出时自动回收资源 # 定时间隔设为60秒,演示时可以改小 async_thread = threading.Thread( target=start_async_loop, args=(async_loop, 60), daemon=True ) async_thread.start() # 主线程保持运行pystray的阻塞逻辑,完全符合平台兼容要求 tray_icon.run()
关键实现细节
- 线程安全:pystray的图标实例本身做了线程安全处理,从后台异步线程修改
icon.icon属性、触发图标刷新不会引发崩溃 - 定时逻辑选择:
- 代码默认采用上次任务执行完成后,再等待60秒触发下一次的逻辑,不会因为爬取耗时变长导致任务堆积、多任务并发更新图标的问题
- 如果需要固定以上次调用时间为基准每60秒触发,只需要在异步循环里用
async_loop.time()计算下次触发的时间点,替换固定时长的sleep即可
- 菜单响应:点击“立即更新”菜单项时,通过
asyncio.run_coroutine_threadsafe把爬取逻辑提交到后台的asyncio循环执行,避免在主线程运行阻塞的网络请求,导致托盘图标卡顿

内容的提问来源于stack exchange,提问作者Jack Deeth
相关产品推荐
相关产品推荐

