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解决方案.
解决方法
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,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处理的内容则不然.所以我想在另一个等待UI事件的线程中使用GLib自己的g_poll()实现(Posix上的一个瘦包装器,在Windows上是一个相当复杂的实现),并通过主线程中的gevent方面同步它一个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,w_false) sock.send('a') except IOError: pass sock.close() class _GPollFD(ctypes.Structure): _fields_ = [("fd",ctypes.c_short)] _poll_func_builder = ctypes.CFUNCTYPE(None,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,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
到目前为止,应用程序运行并对点击和其他用户事件做出正确反应,但窗口内没有任何内容(我可以看到框架和背景缓冲区粘贴到其中).在线程和消息队列的重整中可能缺少某些重绘命令.我不知道如何修复它.有帮助吗?关于如何做的更好的想法?