Python 3 - 主线程未检测到后台线程中的KeyboardInterrupt,直到用户将鼠标悬停在GUI窗口上

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python 3 - 主线程未检测到后台线程中的KeyboardInterrupt,直到用户将鼠标悬停在GUI窗口上相关的知识,希望对你有一定的参考价值。

我编写了一个基于Python 3 TkInter的GUI应用程序,它在后台启动一个工作线程。工作线程完成后,等待两秒钟(这是为了避免可能的竞争条件),然后发送一个KeyboardInterrupt来告诉它可以关闭的主线程。

预期行为:运行程序启动GUI窗口,将一些文本打印到控制台,之后程序自动关闭。

实际行为:只有在用户将鼠标悬停在GUI窗口区域上或按下键盘上的键后才会自动关闭!除此之外,程序运行时没有报告任何错误。

任何人都知道为什么会这样,以及如何解决这个问题?我已经尝试将KeyboardInterrupt包装成一个单独的函数,然后通过timer object调用它,但这会导致相同的行为。

我已经能够在运行Python 3.5.2的两台不同的Linux机器上重现这个问题。和3.6.6分别。

#! /usr/bin/env python3

import os
import threading
import _thread as thread
import time
import tkinter as tk
import tkinter.scrolledtext as ScrolledText

class myGUI(tk.Frame):

    # This class defines the graphical user interface 

    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.root = parent
        self.build_gui()

    def build_gui(self):                    
        # Build GUI
        self.root.title('TEST')
        self.root.option_add('*tearOff', 'FALSE')
        self.grid(column=0, row=0, sticky='ew')
        self.grid_columnconfigure(0, weight=1, uniform='a')

        # Add text widget to display logging info
        st = ScrolledText.ScrolledText(self, state='disabled')
        st.configure(font='TkFixedFont')
        st.grid(column=0, row=1, sticky='w', columnspan=4)

def worker():
    """Skeleton worker function, runs in separate thread (see below)"""  

    # Print some text to console
    print("Working!")
    # Wait 2 seconds to avoid race condition
    time.sleep(2)
    # This triggers a KeyboardInterrupt in the main thread
    thread.interrupt_main()

def main():
    try:
        root = tk.Tk()
        myGUI(root)
        t1 = threading.Thread(target=worker, args=[])
        t1.start()
        root.mainloop()
        t1.join()
    except KeyboardInterrupt:
        # Close program if subthread issues KeyboardInterrupt
        os._exit(0)

main()

(Github Gist链接到上面的脚本here

答案

root.mainloop() mainloop是阻塞的,Python中的挂起(可拦截)信号仅在执行字节码指令之间进行检查。你的代码中的t1.join()实际上永远不会被执行。

由于mainloop阻塞等待转发的硬件中断,因此为了解除阻塞,你必须通过例如像你看到的那样盘旋在窗户上。只有这时解释器才能检测到未决的KeyboardInterrupt。这就是Python中信号处理的工作原理。

解决一般问题可能意味着找到解除阻塞I / O调用的方法,方法是通过外部注入解锁它们所需的内容,或者首先不使用阻塞调用。

对于你的具体设置,你可以使用未处理的SIGTERM杀死整个过程,但当然,这将是非常非常难看的,在这里也是不必要的。如果你只是想找到一种方法来超时你的窗口,你可以使用tkinter.Tk.after方法(显示herehere)超时,或者你摆脱mainloop并自己运行你的循环(here)。

后者可能看起来像:

def main():
    root = tk.Tk()
    myGUI(root)

    t1 = threading.Thread(target=worker, args=[])
    t1.start()

    while True:
        try:
            root.update_idletasks()
            root.update()
            time.sleep(0.1)
        except KeyboardInterrupt:
            print('got interrupt')
            break

    t1.join()

以上是关于Python 3 - 主线程未检测到后台线程中的KeyboardInterrupt,直到用户将鼠标悬停在GUI窗口上的主要内容,如果未能解决你的问题,请参考以下文章

保存在后台线程未完成

无法从 iOS6 中的后台线程调用主线程上的代码

托管对象上下文未保存到持久存储

如何在Python中退出主线程并使子线程在后台继续运行

后台工作人员中异步等待中的调解员死锁-如何检测线程调用自身

通过 Unity 中的后台线程调用主线程中的函数