【问题标题】:How should I get my Tkinter IRC client to continuously read data from the IRC server?我应该如何让我的 Tkinter IRC 客户端不断地从 IRC 服务器读取数据?
【发布时间】:2015-10-18 03:52:23
【问题描述】:

我正在用 python 编写一个小的 IRC 客户端作为练习。我有一个名为 Main 的 Tkinter.Tk 子类来管理整个应用程序,它在其__init__ 方法中创建一个套接字。我在交互模式下玩过套接字,所以我知道如何通过以下方式与 IRC 服务器通信:

>>> s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> s.connect(("irc.foonetic.net", 6667))
>>> s.recv(1000)
":anchor.foonetic.net NOTICE AUTH :*** Looking up your hostname...\r\n:anchor.foonetic.net NOTICE AUTH :*** Couldn't resolve your hostname; using your IP address instead\r\n"
>>> s.send("PASS mypassword\r\n")

也就是说,我使用.send.recv 进行整个对话。因此,为了在我的 Tkinter 应用程序中获取用户输入,我想我将有一个事件处理程序映射到将调用 .send 的 Enter 键。但是我应该在哪里拨打.recv 的电话?我唯一知道怎么做的就是使用计时器每隔几秒调用一次.recv,但这显然不是一个好的解决方案,原因有几个。如果没有数据要接收,我该如何处理.recv 阻塞几秒钟(由您设置的任何超时确定)的事实?我意识到我可以在谷歌上搜索“多线程”,但我想要一些关于这种特定情况的最佳方法的指导。

【问题讨论】:

  • setblocking(0) 在这里可能有用。
  • 这能回答你的问题吗? stackoverflow.com/a/2721734/7432
  • @BryanOakley 我可以看到它可以与 Timer 结合使用,但在我不确定这是否是最好的方法之前没有编写过这种类型的应用程序,这是问题。

标签: python multithreading sockets tkinter


【解决方案1】:

在我的项目中,我为像套接字读/写这样的长期 I/O 设置了一个新线程。要编写一个实用的 GUI 程序,迟早要面对多线程。那是因为 GUI 框架有一个事件队列和一个事件循环。事件循环通常是一个 while 循环,它从事件队列中获取事件并将这些事件分派给已注册的函数。像下面这样:

while event is not QUIT:
    event = event_queue.get(block=True)
    dispatch(event)

dispatch 中,在该事件上注册的所有回调函数都被直接调用。

此类代码在 GUI 线程中工作,如果您在 GUI 回调中执行长期 I/O 或阻塞操作,则该线程在该回调中被阻塞。在事件循环方面,程序被阻塞在调用阻塞回调函数的调度函数中。事件队列中的任何新事件都不会被处理。结果,程序看起来像死了,因为GUI的更新事件被阻止了。

当您设置了一个工作线程来处理耗时的事情时,不要尝试直接从该工作线程操作 GUI 小部件。大多数 GUI 框架都不是线程安全的,它们通过事件队列保持操作顺序。而在非 GUI 线程中操作小部件会破坏这个顺序。

我们可以从非 GUI 线程将事件添加到事件队列中,并让 GUI 线程处理该事件,以保持顺序。这是一些通用语言的正常方式,但对于 python 却不是。在 python 中,函数和方法是第一类对象,所以我们可以将 then 放入队列中。不幸的是,tkinter 的事件队列不支持此功能。

在 Mark Lutz 的 Programming Python 中,对 tkinter 编程进行了很好的介绍。在这本书中,作者介绍了一种在 tkinter 中做多线程的好方法。这是我的演示:

# python3 source code
from tkinter import *
from tkinter.ttk import *
import threading
import time
import queue


root = Tk()
msg = StringVar()
Label(root, textvariable=msg).pack()

# This is our own event queue, each element should be in this form:
# (function_to_be_called_from_gui_thread, function_arguments)
# In python, functions are objects and can be put in a queue.
my_event_queue = queue.Queue()


def worker():
    """
    This is a time consuming worker, it takes 1 second for each task.
    If you put such a worker in the GUI thread, the GUI will be blocked.
    """
    task_counter = 0
    while True:
        time.sleep(1)  # simulate a time consuming task

        # show how many tasks finished in the Label. We put this action in my_event_queue instead of handle
        # it from this worker thread which is not safe. This action will be handled by my_event_handler which is
        # called from GUI thread.
        my_event_queue.put((msg.set, '{} tasks finished.'.format(task_counter)))
        task_counter += 1


def my_event_handler():
    """
    Query my_event_queue, and handle one event per time.
    """
    try:
        func, *args = my_event_queue.get(block=False)
    except queue.Empty:
        pass
    else:
        func(*args)

    # At last schedule handling for next time.
    # Every 100 ms, my_event_handler will be called
    root.after(100, my_event_handler)


threading.Thread(target=worker, daemon=True).start()  # start worker in new thread

my_event_handler()  # start handler, after root.mainloop(), this method will be called every 100ms. Or you can use root.after(100, my_event_handler)

root.mainloop()

这是正在运行的图片。您可以看到我在运行时调整了窗口大小。(好吧,我没有足够的声誉来发布图像,所以您必须自己尝试)

最后我建议你看看Programming Python for tkinter programming。 所有 Python 代码都在 Python3 中。

【讨论】:

    【解决方案2】:

    总的来说,我对 Python 很陌生,对 Tk/ttk 也很陌生。但这是我在 Tk/ttk 中一直在使用的事件触发/信号和工作线程内容的示例。我知道有些人会讨厌单例装饰器,而且我知道还有其他方法可以从其他类调用代码,但触发器类非常方便,工人类的工作方式就像一个魅力。他们一起让事情变得超级简单。

    学分: worker 类是 Pithos 中 GObject worker 的一个非常轻微修改的版本,而单例装饰器是我在 stackoverflow 某处找到的东西的一个非常轻微修改的版本。

    import sys
    import tkinter
    from tkinter import ttk
    from tkinter import StringVar
    import threading
    import queue
    import traceback
    import time
    
    class TkWorkerThreadDemo:
        def __init__(self):
            self.root = tkinter.Tk()
            self.trigger = Trigger.Singleton()
            self.trigger.connect_event('enter_main_thread', self.enter_main_thread)
            self.worker = Worker()
            self.root.title('Worker Thread Demo')
            self.root.resizable(width='False', height='False')
            self.test_label_text = StringVar()
            self.test_label_text.set('')
            self.slider_label_text = StringVar()
            self.slider_label_text.set('Press either button and try to move the slider around...')
            mainframe = ttk.Frame(self.root)
            test_label = ttk.Label(mainframe, anchor='center', justify='center', textvariable=self.test_label_text)
            test_label.pack(padx=8, pady=8, fill='x')
            slider_label = ttk.Label(mainframe, anchor='center', justify='center', textvariable=self.slider_label_text)
            slider_label.pack(padx=8, pady=8, expand=True, fill='x')
            self.vol_slider = ttk.Scale(mainframe, from_=0, to=100, orient='horizontal', value='100', command=self.change_slider_text)
            self.vol_slider.pack(padx=8, pady=8, expand=True, fill='x')
            test_button = ttk.Button(mainframe, text='Start Test with a Worker Thread', command=self.with_worker_thread)
            test_button.pack(padx=8, pady=8)
            test_button = ttk.Button(mainframe, text='Start Test in the Main Thread', command=self.without_worker_thread)
            test_button.pack(padx=8, pady=8)
            mainframe.pack(padx=8, pady=8, expand=True, fill='both')
            self.root.geometry('{}x{}'.format(512, 256))
    
        def enter_main_thread(self, callback, result):
            self.root.after_idle(callback, result)
    
        def in_a_worker_thread(self):
            msg = 'Hello from the worker thread!!!'
            time.sleep(10)
            return msg
    
        def in_a_worker_thread_2(self, msg):
            self.test_label_text.set(msg)
    
        def with_worker_thread(self):
            self.test_label_text.set('Waiting on a message from the worker thread...')
            self.worker.send(self.in_a_worker_thread, (), self.in_a_worker_thread_2)
    
        def in_the_main_thread(self):
            msg = 'Hello from the main thread!!!'
            time.sleep(10)
            self.in_the_main_thread_2(msg)
    
        def in_the_main_thread_2(self, msg):
            self.test_label_text.set(msg)
    
        def without_worker_thread(self):
            self.test_label_text.set('Waiting on a message from the main thread...')
            self.root.update_idletasks()#without this the text wil not get set?
            self.in_the_main_thread()
    
        def change_slider_text(self, slider_value):
            self.slider_label_text.set('Slider value: %s' %round(float(slider_value)))
    
    class Worker:
        def __init__(self):
            self.trigger = Trigger.Singleton()
            self.thread = threading.Thread(target=self._run)
            self.thread.daemon = True
            self.queue = queue.Queue()
            self.thread.start()
    
        def _run(self):
            while True:
                command, args, callback, errorback = self.queue.get()
                try:
                    result = command(*args)
                    if callback:
                        self.trigger.event('enter_main_thread', callback, result)
                except Exception as e:
                    e.traceback = traceback.format_exc()
                    if errorback:
                        self.trigger.event('enter_main_thread', errorback, e)
    
        def send(self, command, args=(), callback=None, errorback=None):
            if errorback is None: errorback = self._default_errorback
            self.queue.put((command, args, callback, errorback))
    
        def _default_errorback(self, error):
            print("Unhandled exception in worker thread:\n{}".format(error.traceback))
    
    class singleton:
        def __init__(self, decorated):
            self._decorated = decorated
            self._instance = None
    
        def Singleton(self):
            if self._instance:
                return self._instance
            else:
                self._instance = self._decorated()
                return self._instance
    
        def __call__(self):
            raise TypeError('Singletons must be accessed through `Singleton()`.')
    
    @singleton
    class Trigger:
        def __init__(self):
            self._events = {}
    
        def connect_event(self, event_name, func, *args, **kwargs):
            self._events[event_name] = func
    
        def disconnect_event(self, event_name, *args, **kwargs):
            if event_name in self._events:
                del self._events[event_name]
    
        def event(self, event_name, *args, **kwargs):
            if event_name in self._events:
                return self._events[event_name](*args, **kwargs)
    
    def main():
        demo = TkWorkerThreadDemo()
        demo.root.mainloop()
        sys.exit(0)
    
    if __name__ == '__main__':
        main()
    

    【讨论】:

      猜你喜欢
      • 2012-10-13
      • 2013-01-16
      • 2013-11-30
      • 1970-01-01
      • 2015-01-27
      • 1970-01-01
      • 1970-01-01
      • 2014-01-11
      • 1970-01-01
      相关资源
      最近更新 更多