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 之类的东西。


根据我的(可能已过时,并且可能受平台限制)经验,您仍然可以摆脱困境,但您需要:

  • 显然只有一个线程可以调用 getchgetmouse 之类的东西。
  • 添加全局 Lock,然后确保每批更新都以 refresh 结尾,并且整个批次都在锁内。
  • 避免对 curs_threads 中提到的功能进行 Python 包装——例如,不要更改 escdelay 或 tabsize。
  • 在启动(退出后)其他线程之前,从主线程初始化(并关闭)屏幕。
  • 如果可能的话,请确保您还在主线程中创建了您需要的所有 windows。 (希望你不想要任何动态弹出子 windows 或任何东西......)

但是 安全 方法是做与 tkinter 或其他不理解线程的 GUI 库相同的事情。它不完全相同,但想法相似。最简单的版本是:

  • 将主线程的工作移至另一个后台线程。
  • 添加 queue.Queue 以便您的后台线程可以请求 curses 命令成为 运行。 (你不需要任何复杂的东西来表示 "command",它只是一个 (func, *args) 元组,因为 Python。)
  • 使主线程循环从队列中弹出命令并调用它们。

如果您的后台线程需要调用 return 一个值的函数,显然您需要稍微复杂一些。你可以看看 multiprocessing.dummy.AsyncResultconcurrent.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 兼容终端。然而,不执行此标准的终端实际上​​已经灭绝,因此这可能是一个安全的假设。