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

这两个Gtk和gevent都具有阻塞分派(dispatch)事件的主循环。如何集成它们的主循环,以便我可以在应用程序上接收网络事件和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毫秒的延迟,并且如果我降低该值太多,则会浪费太多的处理器等待时间。

我希望有一个更好的方法,在此过程中我真正处于 sleep 状态,而没有任何事件可处理。

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

最佳答案

考虑到当前的gevent API,我认为没有通用的解决方案,但是我认为每种平台可能都有特定的解决方案。

Posix解决方案(在Linux下测试)

由于GLib的主循环接口(interface)允许我们设置poll函数,即该函数接受一组文件描述符并在其中一个文件就绪时返回,因此我们定义一个poll函数,该函数依赖gevent的选择来知道文件描述符何时就绪。

Gevent并未公开poll()接口(interface),而select()接口(interface)则有所不同,因此在调用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与主线程中的gevent端进行同步 socket 。这是一种非常丑陋的方法,因为它需要真正的线程(如果正在使用gevent,则可能要使用的greenlet除外)和等待线程侧的普通(非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

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

关于python - 如何将Python的GTK与gevent集成?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/12414874/

10-16 11:15