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解决方案。
发布于 2012-09-14 05:13:32
考虑到当前的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语言中公开g_main_set_poll_func()
,就可以避免这种情况。而且,如果gevent
实现了select()
,它就可以很好地实现poll()
,这将使这个解决方案变得更加简单。
Windows部分解决方案(丑陋和损坏)
所以我考虑在另一个线程中使用GLib自己的g_poll()
实现(在Posix上是一个很薄的包装器,它在Windows上是一个相当复杂的实现)来等待UI事件,并通过TCP套接字将其与主线程中的gevent同步。这是一种非常丑陋的方法,因为它需要真正的线程(除了您可能会使用的greenlet,如果您正在使用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
到目前为止,应用程序运行并对点击和其他用户事件做出了正确的反应,但没有在窗口中绘制任何内容(我可以看到粘贴到其中的框架和背景缓冲区)。线程和消息队列的损坏中可能缺少某些重绘命令。我不知道如何修复它。有什么帮助吗?有没有更好的方法呢?
发布于 2012-09-22 07:54:46
或者,您可以创建一个单独的线程来处理Gevent主循环。您需要一种机制来将安全从一个线程切换到另一个线程。
Stackless
示例代码:
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()
发布于 2012-12-06 18:00:22
如果您使用glib而不是idle_add
,那么对timeout_add
开销的影响就很小(我甚至没有注意到任何影响)。下面是一个使用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()
https://stackoverflow.com/questions/12414874
复制相似问题