用 asyncio 实现多线程异步多任务的界面操作

b4e0f5fffa7dcf7e67f7c6ccfcaed65b.gif

前言

最近有个项目需要从窗口客户端发起的异步多任务请求,为了长时间后台操作不卡死前台操作,则还需要发起多线程来隔离 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
162dd50a9996c146ca9b33133844684a.png

多任务协程

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操作。

扫描二维码关注公众号,回复: 16990179 查看本文章

设置回调函数,用于异步任务结束后触发。

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)
3e23eee0462cf73c2bf2f551407c0a3b.png

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和一组并发子任务task2task3

  • 首先主任务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
694798e38abf429d5ff5dbb765816de4.png

总结一下流程:

  • 获取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执行异步函数。

并发多任务协程

如果各个子任务之间没有依赖关系的时候,可以采用并发执行,大大提高程序的运行效率。

1d3575cf30a2a99b2ec1b2f85220de7c.png

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()
539d2ff42c6fbe9a1555ec34843bfbc1.png

扩展阅读

进一步阅读,可以参考《Async IO in Python: A Complete Walkthrough》,从基本原理到设计模式非常详尽。

https://realpython.com/async-io-python/

源码下载

ff7f529709a6c88f89d6b33937c78ef4.png本期完整源码,可在公众号“深度觉醒”,后台回复:“asyncio”,获取下载链接。

PS:深夜撸码,码农本质上就是个把咖啡转化为代码的生物。不过喝太多咖啡身体扛不住,所以最近戒咖啡,迷上了柚子茶。

推荐一款恒寿堂的蜂蜜柚子茶,买一送一,比韩国的好喝,顺便支持下国货。

7f464902a2b5b5d5406f0ed2dcf2fb8c.gif

猜你喜欢

转载自blog.csdn.net/weixin_47479625/article/details/120793384
今日推荐