6

Gtk は、Python へのバインディングを備えた GUI ツールキットです。Gevent は、libevent (新しいバージョンでは libev) と greenlet の上に構築された Python ネットワーキング ライブラリであり、プロセス全体をブロックすることなく greenlet 内でネットワーク関数を使用できます。

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 ソリューションです。

4

3 に答える 3

8

現在の gevent API を考えると、一般的な解決策はないと思いますが、プラットフォームごとに固有の解決策があると思います。

Posix ソリューション (Linux でテスト済み)

GLib のメイン ループ インターフェイスを使用すると、poll 関数、つまり一連のファイル記述子を取得し、そのうちの 1 つが準備できたときに戻る関数を設定できるため、gevent の select に依存してファイル記述子の準備ができたときに知る poll 関数を定義します。 .

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 の特定のバージョン/名前を知る必要があるため、より良い解決策があるはずだと思います。g_main_set_poll_func()これは、GLibが Python で公開されていれば回避できます。また、 を実装する場合は、 をgevent実装select()することもできますpoll()。これにより、このソリューションがよりシンプルになります。

Windows の部分的な解決策 (醜くて壊れている)

Posix ソリューションは Windows では失敗しますselect()。ネットワーク ソケットでしか機能しないためです。特定の Gtk ハンドルはそうではありません。そこで、別のスレッドで GLib 独自のg_poll()実装 (Posix ではシン ラッパーであり、Windows ではかなり複雑な実装です) を使用して UI イベントを待機し、TCP ソケットを介してメイン スレッドの gevent 側と同期することを考えました。 . これは、真のスレッド (gevent を使用している場合に使用する可能性のある greenlet は別として) と待機スレッド側のプレーン (非 gevent) ソケットを必要とするため、非常に醜いアプローチです。

残念なことに、Windows の UI イベントはスレッドごとに分割されるため、既定では、1 つのスレッドが別のスレッドのイベントを待機することはできません。特定のスレッドのメッセージ キューは、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-13T21:13:32.097 に答える
1

または、Geventメインループを処理するための別のスレッドを作成することもできます。また、あるスレッドから別のスレッドに安全に切り替えるためのメカニズムが必要です。

  1. GUIスレッドからGeventスレッドに安全に切り替えるため(すでに提供されているStacklessに似たもの:StacklessPythonのdiffOSスレッド間の切り替え)。安全に切り替えるための解決策は、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スレッドに戻すのは簡単なはずです。WinApiPostThreadMessageに似たものもgtkに含まれている必要があります...
于 2012-09-21T23:54:45.537 に答える
1

timeout_add代わりにglib を使用する場合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()
于 2012-12-06T10:00:21.623 に答える