【问题标题】:Multithreading curses output in PythonPython中的多线程诅咒输出
【发布时间】:2018-12-21 06:04:40
【问题描述】:

我正在尝试在进度条下方为长时间运行的函数实现一个简单的微调器(使用改编自 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()

为什么输出有时会损坏?是因为单独的线程吗?如果是这样,有什么更好的设计方法建议吗?

【问题讨论】:

  • 您的应用程序的其余部分是否使用诅咒?还是您只是将它用于这一功能?
  • curses 仅用于操作终端输出。应用程序的其余部分使用 Python 和其他模块

标签: python multithreading curses


【解决方案1】:

如果这是唯一您使用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 的终端。但是,不执行此标准的终端实际上​​已经灭绝,因此这可能是一个安全的假设。

【讨论】:

  • 你有关于\e[f 工作原理的资料吗?当我尝试它时,Python 打印字符串文字 "\e[f\e[J"
  • @wcarhart 哎呀,忘了 Python 不理解 \e。请改用\x1b
  • 尽管这个答案并不能完全解决 curses 的问题,但它确实为我解决了问题。
  • 我最终使用了print "\033[A" + next(self.spinner_generator),这与此答案相同。
【解决方案2】:

Python 的 curses 模块所依赖的 curses 库不是线程安全的。

ncurses 有一个curs_threads 功能,这显然是从大约十年前的 5.7 开始就有的。但它需要改变你做一些 API 调用的方式,并链接到-lncursest,而且它仍然不是微不足道的,而且……几乎没有人使用它。

据我所知,没有标准的安装程序或发行版包构建 Python curses 以链接 ncursest — 即使发行版首先包含 ncursest,但他们通常不会这样做。即使他们这样做了,线程安全函数也没有绑定,因此您仍然无法安全地访问诸如设置 tabsize 之类的东西。


根据我(可能已过时,可能受平台限制)的经验,您仍然可以侥幸逃脱,但您需要:

  • 显然只有一个线程可以调用像getchgetmouse 这样的东西。
  • 添加一个全局的Lock,然后确保每批更新都以refresh结尾,并且整批都在Lock里面。
  • 避免使用 Python 包装器围绕 curs_threads 中提到的功能,例如,不要更改 escdelay 或 tabsize。
  • 在启动(退出)其他线程之前,从主线程初始化(并关闭)屏幕。
  • 如果可能,请确保您还在主线程中创建了您需要的所有窗口。 (希望您不想要任何动态弹出子窗口或任何东西……)

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

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

如果您的后台线程需要调用返回值的函数,显然您需要稍微复杂一些。你可以看看multiprocessing.dummy.AsyncResultconcurrent.futures.Future 是如何工作的。或者您甚至可以出于自己的目的窃取Future。但是您可能不需要任何复杂的东西。

如果您正在循环输入,您可能还希望您的主线程执行此操作(这意味着选择“帧速率”并在等待队列和输入之间交替,超时)并调度它,即使你总是分派到同一个线程。

您甚至可以编写一个mtTkinter 样式的包装器,它重现curses 接口(甚至猴子补丁curses 模块),但用将函数和参数放入队列的调用替换每个函数。但我不确定这是否值得。

【讨论】:

  • @ThomasDickey 他们打包了ncursest,或者他们打包了一个Python,其curses 模块是针对ncursest 构建的?
  • 他们打包使用 pthread 支持构建的 ncurses,并使用 ncurses/ncursesw 作为库名称。与 curs_thread 存在的时间相比,Python 的 curses 包装器没有太大变化......
  • @ThomasDickey 但是用于 manylinux 轮子的 libncursesw.so.5 版本肯定是标准的 ncurses,而不是具有不同 API 的线程?还是 OpenSUSE 只是不做 manylinux?
  • 我不知道 - 他们的 ncurses rpm 使用我提到的选项。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-12-14
  • 1970-01-01
  • 1970-01-01
  • 2014-04-23
  • 1970-01-01
  • 2012-04-08
  • 2018-03-28
相关资源
最近更新 更多