【问题标题】:How to integrate Python's GTK with gevent?如何将 Python 的 GTK 与 gevent 集成?
【发布时间】:2012-09-07 01:11:05
【问题描述】:

Gtk 是一个与 Python 绑定的 GUI 工具包。 Gevent 是一个 Python 网络库,建立在 libevent(较新版本的 libev)和 greenlets 之上,允许在 greenlets 中使用网络函数而不会阻塞整个过程。

Gtk 和 gevent 都有分派事件的阻塞主循环。如何集成它们的主循环,以便我可以在我的应用程序上同时接收网络事件和 UI 事件,而不会阻塞另一个?

天真的方法是在 Gtk 的主循环上注册一个空闲回调,当没有 Gtk 事件时调用它。在这个回调中,我们产生了 greenlet,所以网络事件可以发生,同时也给出了一个小的超时,所以进程不会忙等待:

from gi.repository import GLib
import gevent

def _idle():
    gevent.sleep(0.1)
    return True

GLib.idle_add(_idle)

这种方法远非理想,因为我在 UI 事件处理之间有 100 毫秒的延迟,如果我将值降低太多,我会浪费太多的处理器忙于等待。

我想要一种更好的方法,让我的进程真正处于休眠状态,而没有要处理的事件。

PS:我已经找到了一个特定于 Linux 的解决方案(可能也可以在 MacOS 下工作)。我现在真正需要的是一个有效的 Windows 解决方案。

【问题讨论】:

标签: python winapi gtk gevent gtk3


【解决方案1】:

鉴于当前的 gevent API,我认为没有通用的解决方案,但我认为每个平台可能都有特定的解决方案。

Posix 解决方案(Linux 下测试)

由于 GLib 的主循环接口允许我们设置 poll 函数,即获取一组文件描述符并在其中一个准备好时返回的函数,我们定义了一个 poll 函数,它依赖于 gevent 的 select 来知道文件何时描述符已准备就绪。

Gevent没有暴露poll()接口,和select()接口有点不同,所以我们在调用gevent.select.select()的时候要翻译参数和返回值。

使事情复杂一点的是,GLib 没有通过 Python 的接口公开允许该技巧的特定函数 g_main_set_poll_func()。所以我们必须直接使用C函数,为此,ctypes模块就派上用场了。

import ctypes
from gi.repository import GLib
from gevent import select

# Python representation of C struct
class _GPollFD(ctypes.Structure):
    _fields_ = [("fd", ctypes.c_int),
                ("events", ctypes.c_short),
                ("revents", ctypes.c_short)]

# Poll function signature
_poll_func_builder = ctypes.CFUNCTYPE(None, ctypes.POINTER(_GPollFD), ctypes.c_uint, ctypes.c_int)

# Pool function
def _poll(ufds, nfsd, timeout):
    rlist = []
    wlist = []
    xlist = []

    for i in xrange(nfsd):
        wfd = ufds[i]
        if wfd.events & GLib.IOCondition.IN.real:
            rlist.append(wfd.fd)
        if wfd.events & GLib.IOCondition.OUT.real:
            wlist.append(wfd.fd)
        if wfd.events & (GLib.IOCondition.ERR.real | GLib.IOCondition.HUP.real):
            xlist.append(wfd.fd)

    if timeout < 0:
        timeout = None
    else:
        timeout = timeout / 1000.0

    (rlist, wlist, xlist) = select.select(rlist, wlist, xlist, timeout)

    for i in xrange(nfsd):
        wfd = ufds[i]
        wfd.revents = 0
        if wfd.fd in rlist:
            wfd.revents = GLib.IOCondition.IN.real
        if wfd.fd in wlist:
            wfd.revents |= GLib.IOCondition.OUT.real
        if wfd.fd in xlist:
            wfd.revents |= GLib.IOCondition.HUP.real
        ufds[i] = wfd

_poll_func = _poll_func_builder(_poll)

glib = ctypes.CDLL('libglib-2.0.so.0')
glib.g_main_context_set_poll_func(None, _poll_func)

我觉得应该有更好的解决方案,因为这样我们需要知道正在使用的 GLib 的具体版本/名称。如果 GLib 在 Python 中公开g_main_set_poll_func(),则可以避免这种情况。此外,如果gevent 实现select(),它可以很好地实现poll(),这将使这个解决方案更加简单。

Windows部分解决方案(又丑又坏)

Posix 解决方案在 Windows 上失败,因为 select() 只能用于网络套接字,而给定的 Gtk 句柄则不能。所以我想在另一个线程中使用 GLib 自己的g_poll() 实现(Posix 上的瘦包装器,它在 Windows 上是一个相当复杂的实现)来等待 UI 事件,并在主线程中通过TCP 套接字。这是一个非常丑陋的方法,因为它需要真正的线程(除了你可能会使用的greenlets,如果你正在使用gevent)和等待线程端的普通(非gevent)套接字。

Windows 上的 UI 事件太糟糕了,由线程分割,因此默认情况下,一个线程不能等待另一个线程上的事件。在您执行一些 UI 操作之前,不会创建特定线程上的消息队列。所以我不得不在等待线程上创建一个空的 WinAPI 消息框(MessageBoxA())(当然有更好的方法),并用AttachThreadInput() 破坏线程消息队列,这样它就可以看到主线程的事件.所有这些都来自ctypes

import ctypes
import ctypes.wintypes
import gevent
from gevent_patcher import orig_socket as socket
from gi.repository import GLib
from threading import Thread

_poll_args = None
_sock = None
_running = True

def _poll_thread(glib, addr, main_tid):
    global _poll_args

    # Needed to create a message queue on this thread:
    ctypes.windll.user32.MessageBoxA(None, ctypes.c_char_p('Ugly hack'),
                                     ctypes.c_char_p('Just click'), 0)

    this_tid = ctypes.wintypes.DWORD(ctypes.windll.kernel32.GetCurrentThreadId())
    w_true = ctypes.wintypes.BOOL(True)
    w_false = ctypes.wintypes.BOOL(False)

    sock = socket()
    sock.connect(addr)
    del addr

    try:
        while _running:
            sock.recv(1)
            ctypes.windll.user32.AttachThreadInput(main_tid, this_tid, w_true)
            glib.g_poll(*_poll_args)
            ctypes.windll.user32.AttachThreadInput(main_tid, this_tid, w_false)
            sock.send('a')
    except IOError:
        pass
    sock.close()

class _GPollFD(ctypes.Structure):
    _fields_ = [("fd", ctypes.c_int),
                ("events", ctypes.c_short),
                ("revents", ctypes.c_short)]

_poll_func_builder = ctypes.CFUNCTYPE(None, ctypes.POINTER(_GPollFD), ctypes.c_uint, ctypes.c_int)
def _poll(*args):
    global _poll_args
    _poll_args = args
    _sock.send('a')
    _sock.recv(1)

_poll_func = _poll_func_builder(_poll)

# Must be called before Gtk.main()
def register_poll():
    global _sock

    sock = gevent.socket.socket()
    sock.bind(('127.0.0.1', 0))
    addr = sock.getsockname()
    sock.listen(1)

    this_tid = ctypes.wintypes.DWORD(ctypes.windll.kernel32.GetCurrentThreadId())
    glib = ctypes.CDLL('libglib-2.0-0.dll')
    Thread(target=_poll_thread, args=(glib, addr, this_tid)).start()
    _sock, _ = sock.accept()
    sock.close()

    glib.g_main_context_set_poll_func(None, _poll_func)

# Must be called after Gtk.main()
def clean_poll():
    global _sock, _running
    _running = False
    _sock.close()
    del _sock

到目前为止,应用程序运行并正确响应点击和其他用户事件,但在窗口内没有绘制任何内容(我可以看到框架和粘贴到其中的背景缓冲区)。在线程和消息队列的处理中可能缺少一些重绘命令。我不知道如何解决它。有什么帮助吗?有更好的办法吗?

【讨论】:

  • 观察:您可以使用 Mutex,而不是用于在 windows impl 中同步两个线程的套接字。
  • 我相信我尝试使用gevent.coros.Semaphore 进行同步,因为它似乎比丑陋的 TCP 套接字黑客更合适,但它失败了,并显示一条消息说 gevent 不支持多线程。我不能使用非 gevent 感知机制,因为它会阻止 gevent 的循环运行,就像我刚刚在同一个线程中调用 glib 的 g_poll 一样。这就是我使用套接字的原因。
  • 我指的是标准同步对象:thread.EventObject,因为您使用的是真实线程; cores.Semaphore 是 greenlet 的实现,只能在 greenlet 中使用。
  • 这是第二种情况:它会阻塞主线程,而不是让 gevent 安排另一个 greenlet。
【解决方案2】:

或者您可以创建一个单独的线程来处理 Gevent 主循环。而且您需要一种机制来安全地从一个线程切换到另一个线程。

  1. 从 gui 线程安全切换到 Gevent 线程(类似于 Stackless 已经提供的东西:switching between diff OS threads in Stackless Python)。 切换安全的解决方案是使用libev.ev_async

示例代码:

def in_gevent_thread(data_to_send):
  #now you are in gevent thread, so you can use safe greenlet + and network stuff
    ...

def on_button_click():
  #now you are in gui thread
  safe_switch = gevent.core.async(gevent_hub.loop)
  safe_switch.callback = functools.partial(in_gevent_thread, data_to_send)
  safe_switch.send()
  1. 从 gevent 线程切换回 gui 线程应该很容易。与 WinApi PostThreadMessage 类似的东西也应该在 gtk 中......

【讨论】:

  • 这个解决方案是不可接受的,因为它需要我重写应用程序的其余部分,并且会导致太多的同步复杂性,这在像 greenlets 这样的协作线程系统上是可以避免的,而正是在试图整合主循环。
  • 我希望你所有的逻辑都在 gevent 线程中,而 gui 在 gtk 线程中(一个好的架构应该分离这两个概念);所以在 gevent 线程中不需要线程同步,在 gui 线程中也是如此。唯一的要求是 gui 和 logic(gevent) 之间的通信需要一个用于线程切换的异步机制(这个想法已经在我的回答中提出)
  • 这两个概念在不同线程中的隔离是一个“好的架构”,只是因为平台限制不允许这两个在不需要同步的情况下互换使用。这个限制是我试图利用的,所以为了允许这两者之间的无缝集成,将它们“通信”的抽象级别提高到简单的函数调用(在 OO 术语中,是对象之间的消息传递)。您只是建议我回滚到我一开始就试图避免的旧的、更难的和明确的编程模型。
  • 我指的是良好的架构具有单独的层 gui 和逻辑,这并不意味着每个层都应该在单独的线程中运行。因此,有了这种分离,将您的代码调整为 2 个提及线程应该不是很困难;
  • @lvella 如果将这些函数调用包装在较低级别,则可以将通信抽象为简单的函数调用。例如,如果您有一个简单的 send_message / handle_message 协议,您将通过上述函数将消息发送注入 gevent 线程,并将接收到的消息发送回 GUI 并使用gobject.idle_add(message_callback, message)。如果你有一个干净的架构,这个提议应该很容易实现。
【解决方案3】:

如果你使用 glib timeout_add 而不是 idle_add 对 CPU 成本的影响是最小的(我什至没有注意到)。这是 GTK2 + gevent 的完整示例:

import gtk
import gobject

import gevent
from gevent.server import StreamServer
from gevent.socket import create_connection


def handle_tcp(socket, address):
    print 'new tcp connection!'
    while True:
        socket.send('hello\n')
        gevent.sleep(1)


def client_connect(address):
    sockfile = create_connection(address).makefile()
    while True:
        line = sockfile.readline()  # returns None on EOF
        if line is not None:
            print "<<<", line,
        else:
            break


def _trigger_loop():
    gobject.timeout_add(10, gevent_loop, priority=gobject.PRIORITY_HIGH)


def gevent_loop():
    gevent.sleep(0.001)
    _trigger_loop()
    return False


tcp_server = StreamServer(('127.0.0.1', 1234), handle_tcp)
tcp_server.start()
gevent.spawn(client_connect, ('127.0.0.1', 1234))
gevent.spawn(client_connect, ('127.0.0.1', 1234))

_trigger_loop()
gtk.main()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-02-26
    • 1970-01-01
    • 1970-01-01
    • 2013-05-06
    • 2010-11-29
    • 2020-11-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多