如何将Python的GTK与gevent集成?

6

Gtk是一个带有Python绑定的GUI工具包。Gevent是一个基于libevent(新版本上为libev)和greenlets构建的Python网络库,允许在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的解决方案。

你尝试过使用:https://bitbucket.org/RonnyPfannschmidt/gevent-gtkloop 吗? - schlamar
或者你可以调用 gevent.core.loop。这将把繁忙等待降到最低。 - schlamar
3个回答

8

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

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

由于GLib的主循环接口允许我们设置轮询函数,即接受一组文件描述符并在其中一个准备好时返回的函数,因此我们定义了一个依赖于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中公开了,就可以避免这种情况。此外,如果实现了,它也可以实现,这将使解决方案变得更加简单。

Windows部分解决方案(丑陋且不稳定)

由于仅适用于网络套接字,而给定的Gtk句柄不是,因此Posix解决方案在Windows上失败。所以我考虑在另一个线程中使用GLib自己的实现(它是Posix的一个薄包装器,在Windows上是一个相当复杂的实现)等待UI事件,并通过TCP套接字将其与主线程中的gevent一侧同步。这是一种非常丑陋的方法,因为它需要真正的线程(除了您可能正在使用的绿色线程之外,如果您正在使用gevent)和等待线程侧的普通(非gevent)套接字。
遗憾的是,Windows上的UI事件被拆分成线程,因此一个线程默认情况下无法等待另一个线程上的事件。特定线程上的消息队列不会创建,直到您执行一些UI操作。因此,我不得不在等待线程上创建一个空的WinAPI消息框()(肯定有更好的方法),并使用混合线程消息队列,以便它可以看到主线程的事件。所有这些都通过完成。
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

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


观察:在Windows实现中,可以使用Mutex代替用于同步两个线程的套接字。 - Nicolae Dascalu
我相信我尝试使用gevent.coros.Semaphore进行同步,因为它似乎比丑陋的TCP套接字hack更合适,但是它失败了,并显示多个线程不受gevent支持的消息。我不能使用非gevent感知机制,因为它会阻止gevent的循环运行,这与我在同一线程中刚刚调用glib的g_poll几乎相同。这就是为什么我求助于socket的原因。 - lvella
我参考了标准同步对象:thread.EventObject,因为你使用真正的线程;cores.Semaphore是greenlet实现,只应该在greenlet中使用。 - Nicolae Dascalu
这是第二种情况:它会阻塞主线程,而不允许gevent调度另一个greenlet。 - lvella

1

如果你使用 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()

1

或者您可以创建一个单独的线程来处理Gevent主循环。并且您需要一种机制来在不同线程之间安全地切换。

  1. 将安全从GUI线程切换到Gevent线程(类似于Stackless已经提供的在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()

从 gevent 线程切换回 GUI 线程应该很容易。类似于 WinApi 的 PostThreadMessage,在 gtk 中也应该有。

这个解决方案是不可接受的,因为它需要我重写应用程序的其余部分,并且会产生太多同步复杂性,在像greenlets这样的协作线程系统中是可以避免的,而正是尝试集成主循环的关键点。 - lvella
将这两个概念在不同线程中进行隔离是一种“良好的架构”,仅仅因为平台的限制不允许这两者在没有同步的情况下互换使用。我正试图利用这种限制,以便实现这两者之间的无缝集成,提高它们“通信”的抽象级别,使其变成简单的函数调用(在面向对象术语中,即对象之间的消息传递)。你只是建议我回到旧的、更困难和显式的编程模型,而我本来就想避免这种模型。 - lvella
@lvella 如果您在较低级别上包装这些函数调用,可以将通信抽象为简单的函数调用。例如,如果您有一个简单的send_message / handle_message协议,则可以通过上面的函数将消息发送注入到gevent线程中,并使用gobject.idle_add(message_callback,message)将接收到的消息发送回GUI。如果您拥有清晰的架构,那么这个提议应该非常容易实现。 - schlamar
这是一个图形化的总结:http://commons.wikimedia.org/wiki/File:Threaded_loop.png :-) - schlamar
在单独的线程中执行I/O操作总是一个好主意,以下是一些额外的理由:http://www.zeromq.org/whitepapers:design-v01#toc6 - schlamar
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接