Python中的多线程诅咒输出

时间:2018-07-12 22:23:29

标签: python multithreading curses

我正在尝试在进度条下方实现一个简单的微调器(使用从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()

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

2 个答案:

答案 0 :(得分:2)

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

ncurses具有curs_threads功能,该功能自大约十年前的5.7开始就已经存在。但是,这需要更改执行一些API调用的方式,并与-lncursest进行链接,但这仍然不是一件容易的事,而且…几乎没有人使用过它。

据我所知,没有标准的安装程序或发行版程序包会构建Python curses来链接ncursest-即使发行版首先包含ncursest,他们通常也赢得了胜利没错即使这样做,线程安全功能也没有绑定,所以您 still 仍然无法安全地访问诸如设置tabsize之类的内容。


根据我的经验(可能是过时的,并且可能是平台受限的),尽管如此,您还是可以放手一搏,但是您需要:

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

但是 safe 的方法是执行与tkinter或其他不了解线程的GUI库相同的操作。这并不完全相同,但是想法是相似的。最简单的版本是:

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

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

如果要遍历输入,您可能还希望主线程执行此操作(这意味着选择“帧速率”,并在等待队列和输入之间进行超时切换)并分派它,即使您总是分派到同一线程。

您甚至可以编写一个mtTkinter样式的包装程序,该包装程序可以重现curses接口(甚至可以猴子修补curses模块),但是用调用将每个函数替换为放置该函数和args在队列中。但是我不确定这样做是否值得。

答案 1 :(得分:0)

如果这是您使用curses模块的 only 场所,最好的解决方案是停止使用它。

您在此真正使用的curses的唯一功能是其清除屏幕和移动光标的能力。通过直接输出适当的控制序列,例如

sys.stdout.write("\x1b[f\x1b[J" + progress_bar + "\n" + progress_file)

\x1b[f序列将光标移至1,1,\x1b[J清除了从光标位置到屏幕末端的所有内容。

完成屏幕后,无需其他呼叫即可刷新屏幕或重置屏幕。如果需要,您可以再次输出"\x1b[f\x1b[J"来清除屏幕。

诚然,此方法假设用户使用的是VT100兼容终端。但是,不执行此标准的终端实际上​​已经灭绝,因此这可能是一个安全的假设。