除非你有一个非常快的互联网连接,否则你可能会注意到当你按下按钮时,你的应用程序的 GUI 会锁定一点点。这是因为我们发出的网络请求是同步的。当我们的应用程序发出 Web 请求时,它会等待 API 返回响应,然后再继续。在等待时,它不允许应用程序重绘 - 结果,应用程序锁定。
GUI 事件循环
要了解为什么会发生这种情况,我们需要深入了解 GUI 应用程序如何工作的细节。具体情况因平台而异;但无论您使用的是平台还是 GUI 环境,高级概念都是相同的。
从根本上说,GUI 应用程序是一个看起来像这样的单个循环:
while not app.quit_requested():
app.process_events()
app.redraw()
这个循环称为事件循环。(这些不是实际的方法名称——它是对“伪代码”中发生的事情的说明)。
当您单击按钮、拖动滚动条或键入键时,您正在生成一个“事件”。该“事件”被放入队列中,应用程序将在下一次有机会时处理事件队列。响应事件而触发的用户代码称为事件处理程序。这些事件处理程序作为调用的一部分被process_events()调用。
一旦应用程序处理了所有可用的事件,它就会redraw()显示 GUI。这考虑了事件对应用程序显示造成的任何变化,以及操作系统中发生的任何其他事情 - 例如,另一个应用程序的窗口可能会遮盖或显示我们应用程序窗口的一部分,我们的应用程序的重绘需要反映当前可见的窗口部分。
需要注意的重要细节:当应用程序处理一个事件时,它不能重绘,也不能处理其他事件。
这意味着事件处理程序中包含的任何用户逻辑都需要快速完成。用户将观察到完成事件处理程序的任何延迟,作为 GUI 更新的减速(或停止)。如果此延迟足够长,您的操作系统可能会将其报告为问题 - macOS“沙滩球”和 Windows“沙漏”是操作系统告诉您您的应用程序在事件处理程序中花费的时间过长。
“更新标签”或“重新计算输入总数”等简单操作很容易快速完成。但是,有很多操作无法快速完成。如果您正在执行复杂的数学计算,或者为文件系统上的所有文件编制索引,或者执行大型网络请求,您就不能“快速完成”——这些操作本来就很慢。
那么 - 我们如何在 GUI 应用程序中执行长期操作?
异步编程
我们需要一种方法来告诉处于长期事件处理程序中间的应用程序可以暂时将控制权释放回事件循环,只要我们可以从中断的地方继续。由应用决定何时发布此版本;但是如果应用程序定期释放对事件循环的控制,我们可以拥有一个长时间运行的事件处理程序并维护一个响应式 UI。
我们可以通过使用异步编程来做到这一点。异步编程是一种描述程序的方式,它允许解释器同时运行多个函数,在所有同时运行的函数之间共享资源。
异步函数(称为协程)需要显式声明为异步。他们还需要在内部声明何时有机会将上下文更改为另一个协同程序。
在 Python 中,异步编程是使用asyncand await关键字和标准库中的asyncio模块实现的。关键字允许我们async声明一个函数是一个异步协程。await关键字提供了一种方法来声明何时存在将上下文更改为另一个协同程序的机会。asyncio模块为异步编码提供了一些其他有用的工具和原语。
使教程异步
要使我们的教程异步,请修改say_hello()事件处理程序,使其如下所示:
async def say_hello(self, widget):
if self.name_input.value:
name = self.name_input.value
else:
name = 'stranger'
async with httpx.AsyncClient() as client:
response = await client.get("https://jsonplaceholder.typicode.com/posts/42")
payload = response.json()
self.main_window.info_dialog(
"Hello, {}".format(name),
payload["body"],
)
此代码与上一版本相比仅有 4 处更改:
该方法被定义为,而不仅仅是。这告诉 Python 该方法是一个异步协程。async defdef
创建的客户端是异步的AsyncClient(),而不是同步的Client()。这告诉httpx它应该在异步模式下运行,而不是同步模式。
用于创建客户端的上下文管理器被标记为async。这告诉 Python 在进入和退出上下文管理器时有机会释放控制。
get调用是使用await关键字进行的。这指示应用程序在我们等待来自网络的响应时,应用程序可以将控制权释放给事件循环。
Toga 允许您使用常规方法或异步协程作为处理程序;Toga 在幕后管理一切,以确保处理程序被调用或根据需要等待。
如果您保存这些更改并重新运行应用程序(无论是在开发模式下,还是通过更新和重新运行打包的应用程序),应用程序不会有任何明显的变化。但是,当您单击按钮触发对话框时,您可能会注意到一些细微的改进:briefcase dev
按钮返回到“未点击”状态,而不是停留在“点击”状态。
“沙滩球”/“沙漏”图标不会出现
如果您在等待对话框出现时移动/调整应用程序窗口的大小,该窗口将重绘。
如果您尝试打开应用程序菜单,该菜单将立即出现。
下一步
我们现在有了一个灵活且响应迅速的应用程序,即使它正在等待一个缓慢的 API。但它看起来仍然像一个教程应用程序。我们有什么可以做的吗?转到教程 8以了解...