Tkinter 吐槽之一:多线程与 UI 交互
背景
最近想简单粗暴的用 Python 写一个 GUI 的小程序。因为 Tkinter 是 Python 自带的 GUI 解决方案,为了部署方便,就直接选择了 Tkinter。
本来觉得 GUI 发展这么多年以来,就算功能简陋,但也应该大差不差才对,何况我的需求本就十分简单。但事实上 Tkinter 的简陋仍然超出了我的想象。因此想写几篇文章,记录一下踩到的坑,权当吐槽。
问题
众所周知,任何 GUI 程序的主线程中,都是不能执行一些比较耗时的操作的,因为这样的操作会阻塞主线程,造成 GUI 卡顿无响应。因此需要将耗时操作放在其他线程中执行。而在使用多线程机制时,需要能够确保:
- 对数据的修改是需要线程安全的
- 锁或者数据等待不会阻塞主 UI 线程
- 只在主 GUI 线程中修改 GUI
然而 Tkinter 默认是没有提供这样的机制的。它并不能像很多其他框架那样,将线程的 callback 变成主线程的事件,进而在主线程中执行。这就导致了,主线程的 UI 逻辑很难与耗时操作的代码做到良好的解耦。
方案
参考其他 GUI 框架的解决方案,从编码简单的角度来说,多线程与 UI 线程最直观的使用方法,应该是使用 callback 的形式,类似这样的:
def run_in_thread():
time.sleep(1)
return 1
def on_data_received(num):
# change ui
pass
ttk.Button(frame, command=lambda: thread.submit(run_in_thread, callback=on_data_received))
如果忽略 thread 的话,这样的写法和传统的同步代码写法并没有什么区别,类似于 button.on_click = lambda: do_something()
其实,Python 中 concurrent.futures 的 Future 就有类似的方法:
def run_in_thread(n):
time.sleep(n)
return n * 2
def on_data_received(future):
print(future.result())
thread_pool = ThreadPoolExecutor()
future = thread_pool.submit(run_in_thread, 3)
future.add_done_callback(on_data_received)
虽然 Future 是可以 add_callback 的,但这个回调是在子线程中执行的,并非 UI 主线程。这就会打破 Tkinter 的约束:只有主 GUI 线程可以修改 GUI。
其实,参考很多其他的 GUI 的设计来看,解决多线程与 UI 线程交互的问题,归根结底其实只有一种方案:主线程轮询是否有回调事件或者数据改变。不论是 Pub/Sub 的方案,还是 Event 的方案,本质上都是这样一种 loop。因此,我们可以在主 UI 线程中检查其他线程是否完成,进而主动触发回调。
Tkinter 中的 root.after()
正是做这种轮询的最佳方式,类似于:
def check_event(callback: Callable):
if future.done():
callback(future.result())
else:
root.after(100, check_event, callback)
在这段代码里,使用 root.after
通过递归的方式每 100ms 检查一次 future 是否完成,如果执行完成,则直接调用 callback
函数。因为 root.after 一定是在主 UI 线程中执行的,因此 callback
也是在主 UI 线程中执行。
但是每次这么写会比较麻烦,可以参考 js 中 promise
的方法做一些封装,就可以得到一些相对比较通用的工具类:
"""
The module for asynchronous running tasks in Tk GUI.
"""
import tkinter as tk
from concurrent import futures
from typing import Callable, Generic, List, TypeVar
_EVENT_PERIOD_MS = 100
_THREAD_POOL = futures.ThreadPoolExecutor(5, 'pool')
T = TypeVar('T')
class _Promise(Generic[T]):
def __init__(self, future: futures.Future[T]) -> None:
self._future = future
self._on_success = None
self._on_failure = None
def then(self, on_success: Callable[[T], None]):
""" Do something when task is finished. """
self._on_success = on_success
return self
def catch(self, on_failure: Callable[[BaseException], None]):
""" Do something when task is failed. """
self._on_failure = on_failure
return self
class AsyncEvent:
"""
Used for asynchronous tasks in Tk GUI. It takes use of tk.after to check the
event and do the callback in the GUI thread, so we can use it just like
traditional "callback" way.
The class is singleton, so it's shared in the process.
"""
def __init__(self, master: tk.Misc) -> None:
""" Initialize the singleton with Tk.
Args:
master: Same in Tk.
"""
self._master: tk.Misc = master
self._promise_list: List[_Promise] = []
def submit(self, task: Callable[..., T], /, *args) -> _Promise[T]:
"""
Adds an asynchronous task, and return a `Promise` for this task.
We can add callback by the `Promise`.
Args:
task: A function which will be called asynchronously in a thread-pool.
*args: The arguments for the function.
Return: Promise object then you can add callback to it.
"""
if not getattr(self, '_master', None):
raise RuntimeError('Not initialized. Please call init() at first.')
future = _THREAD_POOL.submit(task, *args)
promise: _Promise[T] = _Promise(future)
self._promise_list.append(promise)
# If the len of event list is 1, means that it's not running.
if len(self._promise_list) == 1:
self._master.after(_EVENT_PERIOD_MS, self._handle_event)
return promise
def _handle_event(self):
""" Works as event loop to do the callback. """
for promise in self._promise_list:
future = promise._future
on_success = promise._on_success
on_failure = promise._on_failure
if future.done():
if future.exception():
if on_failure:
on_failure(future.exception() or BaseException())
else:
# add log for the exception.
elif on_success:
on_success(future.result())
self._promise_list.remove(promise)
# Try to handle events in next cycle.
if len(self._promise_list) > 0:
self._master.after(_EVENT_PERIOD_MS, self._handle_event)
这样的话,使用起来就会非常简单:
def run_in_thread(n):
time.sleep(n)
return n * 2
def print_result(n2):
print(n2)
async_executor = AsyncEvent(root)
async_executor.submit(run_in_thread, 3).then(print_result)
这样,整个 coding 比较易读,也满足异步执行的所有要求。
总结
其实类似于 future
或者 promise
的概念在很多语言中都是支持的。在 Tkinter 中,主要的区别在于需要将异步线程的回调运行在主线程当中。因此,可以仿照其他语言的 future
或者 promise
的语法方式来进行封装,从而达到易用且符合要求的目的。
语法糖虽然不是必须的,但好看的代码,终归是赏心悦目的。
Tkinter 吐槽之一:多线程与 UI 交互的更多相关文章
- Tkinter 吐槽之二:Event 事件在子元素中共享
背景 最近想简单粗暴的用 Python 写一个 GUI 的小程序.因为 Tkinter 是 Python 自带的 GUI 解决方案,为了部署方便,就直接选择了 Tkinter. 本来觉得 GUI 发展 ...
- react UI交互 简单实例
<body><!-- React 真实 DOM 将会插入到这里 --><div id="example"></div> <!- ...
- iOS开发笔记7:Text、UI交互细节、两个动画效果等
Text主要总结UILabel.UITextField.UITextView.UIMenuController以及UIWebView/WKWebView相关的一些问题. UI细节主要总结界面交互开发中 ...
- 类似UC天气下拉和微信下拉眼睛头部弹入淡出UI交互效果(开源项目)。
Android-PullLayout是github上的一个第三方开源项目,该项目主页是:https://github.com/BlueMor/Android-PullLayout 原作者项目意图实现 ...
- firefox 扩展开发笔记(三):高级ui交互编程
firefox 扩展开发笔记(三):高级ui交互编程 前言 前两篇链接 1:firefox 扩展开发笔记(一):jpm 使用实践以及调试 2:firefox 扩展开发笔记(二):进阶开发之移动设备模拟 ...
- 拒绝卡顿——在WPF中使用多线程更新UI
原文:拒绝卡顿--在WPF中使用多线程更新UI 有经验的程序员们都知道:不能在UI线程上进行耗时操作,那样会造成界面卡顿,如下就是一个简单的示例: public partial class MainW ...
- 【附案例】UI交互设计不会做?设计大神带你开启动效灵感之路
随着网络技术的创新发展,如今UI交互设计应用越来越广泛,显然已经成为设计的主流及流行的必然趋势.UI界面交互设计中的动效包括移动,滑块,悬停效果,GIF动画等.UI界面交互设计为何越来越受到青睐?它有 ...
- WPF里面多线程访问UI线程、主线程的控件
如果出现以下错误:调用线程无法访问此对象,因为另一个线程拥有该对象. 你就碰到多线程访问UI线程.主线程的控件的问题了. 先占位.
- Android多线程更新UI的方式
Android下,对于耗时的操作要放到子线程中,要不然会残生ANR,本次我们就来学习一下Android多线程更新UI的方式. 首先我们来认识一下anr: anr:application not rep ...
随机推荐
- Beta——发布声明
Beta阶段 1. 新功能: 介绍页面 用户点击软件右上角的 ? 按钮即可看到软件的操作说明! 项目模式 目前软件支持三种模式 空白表单模式.该模式可以生成基于模板的表单数据,也支持生成数据直接训练模 ...
- Javac·编码GBK的不可映射字符
阅文时长 | 0.04分钟 字数统计 | 79.2字符 主要内容 | 1.引言&背景 2.声明与参考资料 『Javac·编码GBK的不可映射字符』 编写人 | SCscHero 编写时间 | ...
- golang:运算符总结
算术运算符 运算符 示例 结果 + 10 + 5 15 - 10 - 5 5 * (除数不能为0) 10 * 5 50 / 10 / 5 2 % (除数不能为0) 10 % 3 1 ++ a = 0; ...
- Rsync忽略文件夹或目录
使用Rsync同步的时候往往会要求对某个文件夹或者文件进行忽略,客户端可以使用--exclude参数来实现对,目录或者文件的忽略 rsync -rltvz --port=873 --exclude & ...
- 一看就懂的 安装完ubuntu 18.04后要做的事情和使用教程
一看就懂的 安装完ubuntu 18.04后要做的事情和使用教程原创CrownP 最后发布于2019-02-05 00:48:30 阅读数 2982 收藏展开1.更改为阿里云的源点击软件和更新 点击其 ...
- stm32之ADC应用实例(单通道、多通道、基于DMA)-转载精华帖,最后一部分的代码是精华
硬件:STM32F103VCT6 开发工具:Keil uVision4 下载调试工具:ARM仿真器网上资料很多,这里做一个详细的整合.(也不是很详细,但很通俗).所用的芯片内嵌3个12位的 ...
- 搜狗拼音输入法v9.6a (9.6.0.3568) 去广告精简优化版本
https://yxnet.net/283.html 搜狗拼音输入法v9.6a (9.6.0.3568) 去广告精简优化版本 软件大小:29.2 MB 软件语言:简体中文 软件版本:去广告版 软件授权 ...
- Docker 的神秘世界
引言 上图就是 Docker 网站的首页,看了这简短的解释,相信你还是不知道它是何方神圣. 讲个故事 为了更好的理解 Docker 是什么,先来讲个故事: 我需要盖一个房子,于是我搬石头.砍木头.画图 ...
- lua中求table长度--(转自有心故我在)
关于lua table介绍,看以前的文章http://www.cnblogs.com/youxin/p/3672467.html. 官方文档是这么描述#的: 取长度操作符写作一元操作 #. 字符串的长 ...
- 为何存在uwsgi还要使用nginx
nginx是对外的服务接口,外部浏览器通过url访问nginx,nginx接收到浏览器发送过来的http请求,将包解析分析url,如果是静态文件则直接访问用户给nginx配置的静态文件目录,直接返回用 ...