Python 中的多线程诅咒输出
Multithreading curses output in Python
我正在尝试在进度条下方实现一个简单的微调器(使用改编自 this answer 的代码),用于长 运行 函数。
[######## ] x%
/ Compressing filename
我在脚本的主线程中有压缩和进度条 运行,在另一个线程中有微调器 运行,因此它实际上可以在压缩发生时旋转。但是,我对进度条和微调器都使用 curses
,并且都使用 curses.refresh()
有时终端会随机输出乱码,我也不知道为什么。我认为这是由于微调器的多线程特性,当我禁用微调器时问题就消失了。
这是微调器的伪代码:
def start(self):
self.busy = True
global stdscr
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
threading.Thread(target=self.spinner_task).start()
def spinner_task(self):
while self.busy:
stdscr.addstr(1, 0, next(self.spinner_generator))
time.sleep(self.delay)
stdscr.refresh()
下面是进度条的伪代码:
progress_bar = "\r[{}] {:.0f}%".format("#" * block + " " * (bar_length - block), round(progress * 100, 0))
progress_file = " {} {}".format(s, filename)
stdscr.clrtoeol()
stdscr.addstr(1, 1, " ")
stdscr.clrtoeol()
stdscr.addstr(0, 0, progress_bar)
stdscr.addstr(1, 1, progress_file)
stdscr.refresh()
然后从 main()
打来电话:
spinner.start()
for each file:
update_progress_bar
compress(file)
spinner.stop()
为什么输出有时会损坏?是因为单独的线程吗?如果是这样,关于更好的设计方法有什么建议吗?
Python 的 curses
模块所依赖的 curses
库不是线程安全的。
ncurses
有一个 curs_threads
特性,它显然自大约十年前的 5.7 以来就已经存在。但它需要改变你进行一些 API 调用的方式,并且 linking 反对 -lncursest
,而且它仍然不是微不足道的,而且......几乎没有人使用过它。
据我所知,没有标准的安装程序或发行版包可以构建 Python curses
到 link ncursest
——即使发行版包含 ncursest
首先,他们通常不会这样做。即使他们这样做了,也没有线程安全函数的绑定,所以你仍然 将无法安全地访问诸如设置 tabsize 之类的东西。
根据我的(可能已过时,并且可能受平台限制)经验,您仍然可以摆脱困境,但您需要:
- 显然只有一个线程可以调用
getch
和 getmouse
之类的东西。
- 添加全局
Lock
,然后确保每批更新都以 refresh
结尾,并且整个批次都在锁内。
- 避免对
curs_threads
中提到的功能进行 Python 包装——例如,不要更改 escdelay 或 tabsize。
- 在启动(退出后)其他线程之前,从主线程初始化(并关闭)屏幕。
- 如果可能的话,请确保您还在主线程中创建了您需要的所有 windows。 (希望你不想要任何动态弹出子 windows 或任何东西......)
但是 安全 方法是做与 tkinter 或其他不理解线程的 GUI 库相同的事情。它不完全相同,但想法相似。最简单的版本是:
- 将主线程的工作移至另一个后台线程。
- 添加
queue.Queue
以便您的后台线程可以请求 curses
命令成为 运行。 (你不需要任何复杂的东西来表示 "command",它只是一个 (func, *args)
元组,因为 Python。)
- 使主线程循环从队列中弹出命令并调用它们。
如果您的后台线程需要调用 return 一个值的函数,显然您需要稍微复杂一些。你可以看看 multiprocessing.dummy.AsyncResult
和 concurrent.futures.Future
是如何工作的。或者您甚至可以窃取 Future
用于您自己的目的。但您可能不需要任何复杂的东西。
如果你在循环输入,你可能还希望你的主线程这样做(这意味着选择一个 "frame rate" 并在等待队列和输入之间交替,超时)并分派它,即使您总是分派到同一个线程。
您甚至可以编写一个 mtTkinter
风格的包装器来重现 curses
接口(或者甚至对 curses
模块进行猴子修补),但用调用 put 函数替换每个函数和队列中的参数。但我不确定这是否值得付出努力。
如果这是您使用 curses
模块的唯一 地方,最好的解决方案是停止使用它。
您在这里真正使用的 curses
的唯一功能是它能够清除屏幕和移动光标。这可以很容易地通过直接输出适当的控制序列来复制,例如:
sys.stdout.write("\x1b[f\x1b[J" + progress_bar + "\n" + progress_file)
\x1b[f
序列将光标移动到1,1,\x1b[J
清除光标位置到屏幕末尾的所有内容。
无需额外调用即可刷新屏幕或在完成后重置屏幕。如果需要可以再次输出"\x1b[f\x1b[J"
清屏
无可否认,这种方法假设用户使用的是 VT100 兼容终端。然而,不执行此标准的终端实际上已经灭绝,因此这可能是一个安全的假设。
我正在尝试在进度条下方实现一个简单的微调器(使用改编自 this answer 的代码),用于长 运行 函数。
[######## ] x%
/ Compressing filename
我在脚本的主线程中有压缩和进度条 运行,在另一个线程中有微调器 运行,因此它实际上可以在压缩发生时旋转。但是,我对进度条和微调器都使用 curses
,并且都使用 curses.refresh()
有时终端会随机输出乱码,我也不知道为什么。我认为这是由于微调器的多线程特性,当我禁用微调器时问题就消失了。
这是微调器的伪代码:
def start(self):
self.busy = True
global stdscr
stdscr = curses.initscr()
curses.noecho()
curses.cbreak()
threading.Thread(target=self.spinner_task).start()
def spinner_task(self):
while self.busy:
stdscr.addstr(1, 0, next(self.spinner_generator))
time.sleep(self.delay)
stdscr.refresh()
下面是进度条的伪代码:
progress_bar = "\r[{}] {:.0f}%".format("#" * block + " " * (bar_length - block), round(progress * 100, 0))
progress_file = " {} {}".format(s, filename)
stdscr.clrtoeol()
stdscr.addstr(1, 1, " ")
stdscr.clrtoeol()
stdscr.addstr(0, 0, progress_bar)
stdscr.addstr(1, 1, progress_file)
stdscr.refresh()
然后从 main()
打来电话:
spinner.start()
for each file:
update_progress_bar
compress(file)
spinner.stop()
为什么输出有时会损坏?是因为单独的线程吗?如果是这样,关于更好的设计方法有什么建议吗?
Python 的 curses
模块所依赖的 curses
库不是线程安全的。
ncurses
有一个 curs_threads
特性,它显然自大约十年前的 5.7 以来就已经存在。但它需要改变你进行一些 API 调用的方式,并且 linking 反对 -lncursest
,而且它仍然不是微不足道的,而且......几乎没有人使用过它。
据我所知,没有标准的安装程序或发行版包可以构建 Python curses
到 link ncursest
——即使发行版包含 ncursest
首先,他们通常不会这样做。即使他们这样做了,也没有线程安全函数的绑定,所以你仍然 将无法安全地访问诸如设置 tabsize 之类的东西。
根据我的(可能已过时,并且可能受平台限制)经验,您仍然可以摆脱困境,但您需要:
- 显然只有一个线程可以调用
getch
和getmouse
之类的东西。 - 添加全局
Lock
,然后确保每批更新都以refresh
结尾,并且整个批次都在锁内。 - 避免对
curs_threads
中提到的功能进行 Python 包装——例如,不要更改 escdelay 或 tabsize。 - 在启动(退出后)其他线程之前,从主线程初始化(并关闭)屏幕。
- 如果可能的话,请确保您还在主线程中创建了您需要的所有 windows。 (希望你不想要任何动态弹出子 windows 或任何东西......)
但是 安全 方法是做与 tkinter 或其他不理解线程的 GUI 库相同的事情。它不完全相同,但想法相似。最简单的版本是:
- 将主线程的工作移至另一个后台线程。
- 添加
queue.Queue
以便您的后台线程可以请求curses
命令成为 运行。 (你不需要任何复杂的东西来表示 "command",它只是一个(func, *args)
元组,因为 Python。) - 使主线程循环从队列中弹出命令并调用它们。
如果您的后台线程需要调用 return 一个值的函数,显然您需要稍微复杂一些。你可以看看 multiprocessing.dummy.AsyncResult
和 concurrent.futures.Future
是如何工作的。或者您甚至可以窃取 Future
用于您自己的目的。但您可能不需要任何复杂的东西。
如果你在循环输入,你可能还希望你的主线程这样做(这意味着选择一个 "frame rate" 并在等待队列和输入之间交替,超时)并分派它,即使您总是分派到同一个线程。
您甚至可以编写一个 mtTkinter
风格的包装器来重现 curses
接口(或者甚至对 curses
模块进行猴子修补),但用调用 put 函数替换每个函数和队列中的参数。但我不确定这是否值得付出努力。
如果这是您使用 curses
模块的唯一 地方,最好的解决方案是停止使用它。
您在这里真正使用的 curses
的唯一功能是它能够清除屏幕和移动光标。这可以很容易地通过直接输出适当的控制序列来复制,例如:
sys.stdout.write("\x1b[f\x1b[J" + progress_bar + "\n" + progress_file)
\x1b[f
序列将光标移动到1,1,\x1b[J
清除光标位置到屏幕末尾的所有内容。
无需额外调用即可刷新屏幕或在完成后重置屏幕。如果需要可以再次输出"\x1b[f\x1b[J"
清屏
无可否认,这种方法假设用户使用的是 VT100 兼容终端。然而,不执行此标准的终端实际上已经灭绝,因此这可能是一个安全的假设。