前言
最近有个项目需要从窗口客户端发起的异步多任务请求,为了长时间后台操作不卡死前台操作,则还需要发起多线程来隔离 UI 主界面事件循环和 asyncio 后台异步任务事件循环。
支持库简介
1. 异步协程库 asyncio
asyncio
采用协程的方式来实现事件循环,进而达到异步任务的效果。
pip install asyncio
协程使用@asyncio.coroutine
装饰器来标记异步任务,使用yield from
来等待异步任务完成。其中
@asyncio.coroutine === async
yield from === await
自从引入了 async/await
关键字以后,代码实现更是优雅了很多。
2. 界面库 wxPython
wxPython是一个跨平台的GUI工具包为Python编程语言。它允许 Python 程序员简单轻松地创建具有强大、功能强大的图形用户界面的程序。它是作为一组 Python 扩展模块实现的,这些模块包装了流行的wxWidgets跨平台库的 GUI 组件,该 库是用 C++ 编写的。
pip install wxpython
特别是在windows
下,能实现很多独特酷炫的窗口效果,具体可以运行 wxDemo
示例参考。
# 运行 python目录下/Scripts/wxdemo.exe
C:\ProgramData\Anaconda3\envs\ctrip_code36\Scripts>wxdemo.exe
多任务协程
1. 创建异步任务
我们先简单创建一个异步任务,随机一个任务时长,并记录任务起始时间和结束时间。
async def AsyncTask(self, name):
t = random.randint(1, 5)
self.log('Start %s: %.8f, duration %ds' % (name, time.time() - self.start, t))
await asyncio.sleep(t) # 模拟长时间后台操作
self.log('End %s: %.8f' % (name, time.time() - self.start))
return name
async
关键字来定义异步函数,await
则等待操作返回。这里使用asyncio.sleep()
来模拟真正的IO
操作。
设置回调函数,用于异步任务结束后触发。
def task_callback(self, task):
time.sleep(1)
self.log('Callback %s: %.8f' % (task.result(), time.time() - self.start))
2. 构建窗口界面
绘制一个简单的窗口,包含一个按钮来触发异步任务,一个静态文本来显示当前任务状态。
class MyFrame(wx.Frame):
def __init__(
self, parent, ID, title, pos=wx.DefaultPosition,
size=wx.DefaultSize, style=wx.DEFAULT_FRAME_STYLE
):
wx.Frame.__init__(self, parent, ID, title, pos, size, style)
panel = wx.Panel(self, -1)
self.start = time.time() # 记录初始时间戳
btn = wx.Button(panel, -1, "启动异步任务", size=(100, 30))
btn.SetPosition((20, 20))
txt = wx.StaticText(panel, -1, "任务未运行", (200, 40))
txt.SetPosition((30, 70))
self.txt = txt
self.Bind(wx.EVT_BUTTON, self.OnButton, btn)
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
3. 创建线程
获取asyncio
的事件循环,发起一个独立的线程,隔离主界面事件
def OnButton(self, evt):
async_loop = asyncio.get_event_loop()
if not async_loop.is_running():
threading.Thread(target=self._asyncio_thread, args=(async_loop,)).start()
return
4. 创建主任务
设置一个主任务,里面包含了一个独立的子任务task1
和一组并发子任务task2
和task3
:
首先主任务
Master task
启动;启动了一个 5 秒的子任务
task1
;完成后同时启动了 5 秒的子任务
task2
和 4 秒的子任务task3
,并设置了回调函数;接着
task3
任务时间短,先完成,并触发了任务回调;然后
task2
完成,执行了对应的回调;最后
Master task
退出。
def _asyncio_thread(self, async_loop):
self.log('Start %s: %.8f' % ('Master task', time.time() - self.start))
asyncio.set_event_loop(async_loop)
print('异步单任务', 'task1')
task = asyncio.ensure_future(self.AsyncTask('task1'))
async_loop.run_until_complete(task)
print('异步多任务并发', 'task2', 'task3')
tasks = []
task = asyncio.ensure_future(self.AsyncTask('task2'))
task.add_done_callback(self.task_callback)
tasks.append(task) # 将多个任务对象装在到一个任务列表中
task = asyncio.ensure_future(self.AsyncTask('task3'))
task.add_done_callback(self.task_callback)
tasks.append(task)
async_loop.run_until_complete(asyncio.wait(tasks))
self.log('End %s: %.8f' % ('Master task', time.time() - self.start))
return
总结一下流程:
获取
asyncio
的事件循环async_loop = asyncio.get_event_loop()
;发起一个线程,将
async_loop
注入到子线程中;在子线程内设置事件循环,隔离主界面事件循环
asyncio.set_event_loop(async_loop);
用
asyncio.ensure_future
配置异步任务;用
add_done_callback
设置回调函数;用
async_loop.run_until_complete
执行异步函数。
并发多任务协程
如果各个子任务之间没有依赖关系的时候,可以采用并发执行,大大提高程序的运行效率。
1. 创建多个异步任务
def OnButton(self, evt):
async_loop = asyncio.get_event_loop()
if not async_loop.is_running():
t = threading.Thread(target=self._asyncio_thread, args=(async_loop,))
t.start()
for i in range(5):
asyncio.run_coroutine_threadsafe(self.AsyncTask('task%d' % i), async_loop)
2. 创建线程
def _asyncio_thread(self, async_loop):
asyncio.set_event_loop(async_loop)
async_loop.run_forever()
3. 退出事件循环
值得注意的是,由于线程中的async_loop
是以run_forever()
启动,那在关闭程序时,需要执行call_soon_threadsafe(async_loop.stop)
做相应的清理操作,才能保证程序顺利退出。
def OnCloseWindow(self, evt):
async_loop = asyncio.get_event_loop()
async_loop.call_soon_threadsafe(async_loop.stop)
self.Destroy()
扩展阅读
进一步阅读,可以参考《Async IO in Python: A Complete Walkthrough》,从基本原理到设计模式非常详尽。
https://realpython.com/async-io-python/
源码下载
本期完整源码,可在公众号“深度觉醒”,后台回复:“asyncio”,获取下载链接。
PS:深夜撸码,码农本质上就是个把咖啡转化为代码的生物。不过喝太多咖啡身体扛不住,所以最近戒咖啡,迷上了柚子茶。
推荐一款恒寿堂的蜂蜜柚子茶,买一送一,比韩国的好喝,顺便支持下国货。