【问题标题】:Trying to run a thread with tkinter (tkinter is on the main thread) performing a task but instead it halts the main thread尝试使用 tkinter 运行线程(tkinter 在主线程上)执行任务,但它停止了主线程
【发布时间】:2013-02-19 00:33:21
【问题描述】:

我是 Python 新手,目前正在开发一个供个人使用的小型应用程序。我正在为我的 gui 使用 tkinter。

我要做的是创建一个带有标签的顶级弹出窗口,该弹出窗口根据登录尝试的方式更改文本。因此,当运行 tk 的主线程显示带有动态文本的弹出窗口时,我想启动一个线程以尝试最多登录 5 次,并通过设置名为“logindata”的全局变量向主线程报告。

AuctioneerGUI 中的 _login() 方法和 LoginThread 类实际上是这里唯一重要的事情,您可以忽略其余的,但它们可能认为相关。

_login() 方法在登录按钮被按下时被调用。所有这一切都是尝试登录并设置登录数据。主线程同时循环,直到它注意到 LoginThread 已经设置了变量,当它收集了所有三个时,它将进入其余的逻辑(没有完全实现,但与问题无关)

现在发生的情况是主线程在 LoginThread 启动后停止,只有在它完成后才会继续。即使 LoginThread 应该在单独的线程中运行,因此不会停止主线程。因此,只有在 LoginThread 执行的任务完成后才会显示弹出窗口。我希望弹出窗口只出现并显示为用户提供更新的标签。我该怎么做?

我确定问题是线程停止了主线程,因为我使用打印确定了这一点。

我还有一个小问题。 popup.destroy() 似乎没有做任何事情。 TopLevel 就在那里。

对不起,文字墙,并提前感谢您帮助我。我已经花了比我应该尝试几种不同的东西更多的时间,但我没有设法让它发挥作用。

如果有什么不清楚的地方,请告诉我,不要介意有时效率低下或愚蠢的逻辑,我首先想让它至少能正常工作,然后再让它变得漂亮。

-大安

global logindata
logindata = {"counter": -1, "status": -1, "success": -1}

class AuctioneerGUI:
    def __init__(self):
        root = Tk()
        root.title("Path of Exile Auctioneer")
        self._setupGUI(root)
        self._loggingin = False

        root.protocol("WM_DELETE_WINDOW", lambda: root.quit())
        root.mainloop()           

    def _setupGUI(self, root):            
        frame = Frame(root)

        email = StringVar()
        pass_ = StringVar()
        thread = StringVar()

        email.set("email")
        pass_.set("password")
        thread.set("76300")

        email_label = Label(frame, text="email")
        self._email_box = Entry(frame, takefocus=True, width=50, textvariable=email)
        self._email_box.focus_set()
        pass_label = Label(frame, text="password")
        self._pass_box = Entry(frame, takefocus=True, show="*", width=50, textvariable=pass_)
        thread_label = Label(frame, text="thread id")
        self._thread_box = Entry(frame, takefocus=True, width=10, textvariable=thread)
        self._login_button = Button(frame, text="login", command=lambda: self._login(root), takefocus=True)

        frame.pack()
        email_label.pack()
        self._email_box.pack()
        pass_label.pack()
        self._pass_box.pack()
        thread_label.pack()
        self._thread_box.pack()
        self._login_button.pack()

    def _login(self, root):
        self._login_button.configure(command=None)
        email = self._email_box.get()
        pass_ = self._pass_box.get()
        thread = self._thread_box.get()
        # Check email validity
        # no whitespaces, 1 @ sign 1 . after the @ sign
        try:
            thread = int(thread)
        except ValueError:
            return -1
            #invalid thread

        if not re.match(r"[^@]+@[^@]+\.[^@]+", email) or not email.find(" ") == -1:
            return -1
            #invalid mail

        self._sm = SessionManager(email, pass_, thread)    

        self._message = StringVar()
        self._message.set("Attempt 1/5.")

        popup = Toplevel(root)
        popup.title("Logging in...")
        message_label = Label(popup, text = self._message.get(), textvariable = self._message)
        message_label.pack()

        _thread = LoginThread(self._sm)        
        _thread.start()

        loop = True                

        while loop:
            counter = -1
            success = -1
            status = -1
            while counter == -1:
                counter = logindata["counter"]
                print(counter)
            while success == -1:
                success = logindata["success"]
            print(success)
            while status == -1:
                status = logindata["status"]
            print(status)
            if success:
                self._message.set("Attempt {}/5. Success.".format(counter))
            elif status == 200:
                self._message.set("Attempt {}/5. Failed: wrong password.".format(counter))
            else:
                self._message.set("Attempt {}/5. Failed: connection error. {}".format(counter, status))
            updatebar = not success
            logindata["counter"] = -1
            logindata["status"] = -1
            logindata["success"] = -1
            if counter == 5:
                break

        popup.destroy()
        self._login_button["command"] = lambda: self._login(root)
        self._setup_main_layout(root)

    def _setup_main_layout(self, root):
        pass

class LoginThread(threading.Thread):

    def __init__(self, sessionmanager):
        threading.Thread.__init__(self)
        self._sm = sessionmanager

    def run(self):
        success = False
        counter = 1
        while not success:
            if counter > 5:
                break

            data = self._sm.login()
            status = data[1]
            success = data[0]
            logindata["counter"] = counter
            logindata["success"] = success
            logindata["status"] = status
            counter += 1
            print("done")

更新:

经过一些研究,我将通过创建一个继承自标签的 ThreadSafeLabel 来解决该问题,该标签通过管道传输到 Widget 并通过队列进行通信,如下例所示:

http://effbot.org/zone/tkinter-threads.htm

【问题讨论】:

  • 理解为什么会这样是有用的。您仍在轮询是否完成,这仍然很浪费,但您只是每 100 毫秒执行一次,而不是尽可能快。而且,更重要的是,您让tkinter 主事件循环在中间的 100 毫秒内运行,因此 GUI 不会冻结。这仍然不理想,但它是您可以使用纯 tkinter 做的最好的事情。
  • 是的,我意识到通过尽可能快的轮询来囤积所有 cpu 资源并不聪明。我发布的代码 sn-p 几乎是一个正在进行中的混乱。我有一些使用 java 线程的经验,但我从来没有使用过像 tkinter 这样处理它的东西。
  • 我可能记错了,但是 Swing 基本上不一样——从另一个线程中触摸任何 Swing 对象会导致异常或神秘的崩溃等?除了我很确定 Swing 有一个 runLaterOnDispatchThread 方法(以及像 BackgroundWorkerWorkerExecutor 这样的包装器),对吧?

标签: python multithreading python-3.x tkinter


【解决方案1】:

Zeroth,正如 unutbu 指出的那样,你只是在主线程中运行另一个线程的 run 函数,所以在它完成之前什么都不会发生。


一旦你解决了这个问题,你就永远不会想要在等待变量更改时让线程旋转,就像你在这里所做的那样:

while counter == -1:
    counter = logindata["counter"]
    print(counter)

主线程不可能做任何事情,只能在这里旋转,直到后台线程将logindata["counter"] 设置为其他东西。如果你强制主线程等到另一个线程完成,你还不如在主线程中运行其他代码。您的代码与单线程处理具有相同的效果,除了它无缘无故地一遍又一遍地检查值会消耗尽可能多的 CPU。

如果您需要等到某事完成,您需要使用某种跨线程信号,例如threading.Conditionqueue.Queue


但是,这仍然不能解决您的问题,因为主线程将仍然卡在_login 函数中,直到登录完成。这意味着它不能做其他事情,比如重绘屏幕、处理鼠标点击等。

因此,即使您解决了前两个问题并让事情正常进行,这仍然与不生成线程并仅在主线程中进行登录完全相同。

您需要的是一个_login 函数,它在启动后台线程后立即返回,然后使用其他机制从后台线程触发 tkinter 循环上的事件。

【讨论】:

  • 谢谢。我将对此进行调查并进行更多研究,听起来这应该可以解决我的问题,我现在看到了明显的问题,即那些其他 while 循环仍在主线程中。
  • @DaanLubbers:tkinter 的最大问题是它没有线程安全的run_function_in_main_thread,甚至没有线程安全的add_event_to_queue 方法可以用来实现一个你自己。您必须进行一些搜索才能了解如何操作(尽管答案可能在右侧的相关问题之一上)。或者搜索 mktkinter,它神奇地将所有主线程外调用变成“在主线程上运行此函数”调用。
【解决方案2】:

启动threading.Thread 的正确方法是调用start 方法,而不是run 方法。产生一个新线程的是start 方法。没有它,你实际上是在主线程中运行LoginThread.run

那就试试吧:

    _thread = LoginThread(self._sm)        
    _thread.start()

来自the docs

一旦创建了线程对象,它的活动必须通过调用来启动 线程的 start() 方法。这会调用 run() 方法 单独的控制线程。

【讨论】:

  • 我不知道这是怎么回事。我什至在文中称它为开始,我很确定它是从某个时候开始的。使用 start 尽管弹出窗口出现,但它根本不显示。我按下登录按钮,它在 http 内容完成时保持按下状态一秒钟左右,然后它松开并将正确的数据发送到主线程。所以它似乎仍然以某种方式阻塞了主线程。还是它会在弹出窗口呈现之前以某种方式关闭?
猜你喜欢
  • 2017-10-10
  • 2017-11-21
  • 2021-01-27
  • 1970-01-01
  • 2011-08-05
  • 1970-01-01
  • 2023-03-05
  • 1970-01-01
  • 2013-05-05
相关资源
最近更新 更多