Hello everyone,

I've recently had reason to investigate how to do asynchronous
notification from worker-threads to a main-thread (which handles all
gtk related function calls). After searching the web and mailing-lists
for pygtk for examples on this topic I found out about the following
possibilities:

1. Using gobject.io_add_watch() to watch a filehandle for activity.
There are several variations on this one:
1.1 Use a temporary file. (Doesn't work on windows.)
1.2 Use a AF_INET socket. (Does work on windows, but many "personal
firewalls" get suspicious about it, so an alternative solution is
probably preferred.)
1.3  Use os.pipe(). (This uses an extra thread per fd, due to glib's
gio_channel stuff, but otherwise should work fine.)
2. Lock the gdk-lock inside worker-thread and schedule an function to
be called within main-thread using gobject.idle_add() .
3. Use the gsource-module to make pygtk able to listen to a win32
event. It can be found here:
http://www.daa.com.au/pipermail/pygtk/2004-February/006961.html
I have briefly tested this technique, but didn't get it to work
reliably. This otherwise seems like a very good solution.

I've written a test-program which shows the points 2 and 1.3.  Both
work fine on my linux installation (debian unstable, python 2.3.5,
pygtk 2.4.1), however the example using pipes hangs when run in
windows (windows xp, python 2.3.4, pygtk 2.4.1).

Anyway, I've found a solution (point 2) which works reliably for me.
Maybe it would be good to document that it is not possible to use
os.pipe() with the gobject.io_add_watch() function on windows.

Any alternative solutions to the original problem apart from the ones
listed above would be greatly appreciated.

//Hugo
import pygtk
pygtk.require('2.0')

import gtk, gtk.gdk, gobject

import threading, time, sys, os

def call_with_locks(func):
        """ This function wraps a function with the gtk/gdk locks held. """
        gtk.gdk.threads_enter()
        func()
        gtk.gdk.threads_leave()

class WorkerThread(threading.Thread):
        def __init__(self):
                threading.Thread.__init__(self)
                self.input = []
                self.input_cond = threading.Condition()
                self.callback = None
                
        def run(self):
                """ Wait until someone calls add(), if so sleep for a second, 
and
                call notify_main_thread()
                """
                while 1:
                        self.input_cond.acquire()
                        while (len(self.input) == 0):
                                self.input_cond.wait()
                        item = self.input[0]
                        self.input = self.input[1:]
                        self.input_cond.release()
                        if item == 0:
                                return
                        else:
                                time.sleep(1)
                                self.notify_main_thread()
                                
        def notify_main_thread(self):
                """ This function is implemented by derived classes. It does 
something
                to notify the main thread.
                """
                raise NotImplementedError
                
        def _add(self, msg):
                """ Adds a message to internal queue. """
                self.input_cond.acquire()
                self.input.append(msg)
                self.input_cond.notify()
                self.input_cond.release()
                
        def add(self):
                """ Wakes up worker thread. """
                self._add(1)
        
        def stop(self):
                """ Stops the thread and joins it. """
                self._add(0)
                self.join()
                
class WorkerThreadIdle(WorkerThread):
        def notify_main_thread(self):
                gtk.gdk.threads_enter()
                gobject.idle_add(call_with_locks, self.callback)
                gtk.gdk.threads_leave()
                
class WorkerThreadPipe(WorkerThread):
        def __init__(self, fd_w):
                WorkerThread.__init__(self)
                self.fd_w = fd_w
        
        def notify_main_thread(self):
                # are this lock really needed? We're only writing to a file..
                gtk.gdk.threads_enter() 
                os.write(self.fd_w, "a")
                gtk.gdk.threads_leave()

class MainWindow(gtk.Window):
        def __init__(self, worker_thread):
                gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
                self.worker_thread = worker_thread
                self.dialog = gtk.Dialog(
                                        "My dialog",
                     self,
                     gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
                     (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT))
                self.vbox = gtk.VBox()
                self.button1 = gtk.Button("dialog.run() directly from main 
thread")
                self.button1.connect("clicked", self.on_button1_push)
                self.vbox.pack_start(self.button1)
                self.button2 = gtk.Button("dialog.run() in idle handler with 
gdk-lock")
                self.button2.connect("clicked", self.on_button2_push)
                self.vbox.pack_start(self.button2)
                self.button3 = gtk.Button("dialog.run() in idle handler WITHOUT 
gdk-lock")
                self.button3.connect("clicked", self.on_button3_push)
                self.vbox.pack_start(self.button3)
                self.add(self.vbox)
                self.connect("delete-event", gtk.main_quit)
        
        def start(self):
                """ Shows this window and starts gtk_main()"""
                self.show_all()
                gtk.main()
                
        def notify_worker_thread(self):
                self.worker_thread.add()
                self.dialog.show_all()
                self.dialog.run()
                self.dialog.hide()
        
        def on_button1_push(self, widget, data = None):
                # notify worker thread in main thread
                self.notify_worker_thread()

        def on_button2_push(self, widget, data = None):
                # notify worker thread in an idle handler of gtk's main loop
                # since we're in an idle handler, we're called in the main 
thread, 
                # BUT we don't have the gdk-lock. Therefor we need to acquire 
it 
                # before we can notify worker thread. 
                gobject.idle_add(call_with_locks, self.notify_worker_thread)
                
        def on_button3_push(self, widget):
                # This does exactly the same as above, but doesn't acquire the 
gdk-lock
                # before. This means that something messes upp. 
                gobject.idle_add(self.notify_worker_thread)
        
        def thread_callback(self):
                """ Will be called in main thread by an idle callback with 
                gdk-lock held. """
                self.dialog.response(1)
                
class MainWindowIdle(MainWindow):
        pass
        
class MainWindowPipe(MainWindow):
        def __init__(self, worker_thread, fd_r):
                MainWindow.__init__(self, worker_thread)
                self.fd_r = fd_r
                watch_id = gobject.io_add_watch(fd_r, gobject.IO_IN | 
gobject.IO_PRI    | gobject.IO_ERR | gobject.IO_HUP, self.on_io_activity)

        def on_io_activity(self, fd, condition, data = None):
                if condition & gobject.IO_HUP:
                        print "hup"
                        return gtk.FALSE
                elif condition & gobject.IO_ERR:
                        print "err"
                        return gtk.FALSE
                elif condition & gobject.IO_IN:
                        print "CONDITION = IO_IN"
                        print fd
                        a = os.read(fd, 1) # <--- this blocks!
                        print "AFTER os.read()"
                        self.thread_callback()
                        return gtk.TRUE
                else:
                        print "else"
                        return gtk.FALSE
        

if __name__ == "__main__":
        gtk.threads_init()
        
        if len(sys.argv) < 2:
                print "Usage thisprogram variant"
                print " variant == 1 => idle"
                print " variant == 2 => pipe"
                sys.exit(1)
        else: 
                variant = int(sys.argv[1])
        
        if variant == 1:
                worker_thread = WorkerThreadIdle()
                mw = MainWindowIdle(worker_thread)
        elif variant == 2:
                (fd_r, fd_w) = os.pipe()
                worker_thread = WorkerThreadPipe(fd_w)
                mw = MainWindowPipe(worker_thread, fd_r)
        else:
                print "wrong variant!"
                sys.exit(1)

        worker_thread.callback = mw.thread_callback
        worker_thread.start()
        
        gtk.gdk.threads_enter()
        mw.start()
        gtk.gdk.threads_leave()
        
        worker_thread.stop()

_______________________________________________
pygtk mailing list   [email protected]
http://www.daa.com.au/mailman/listinfo/pygtk
Read the PyGTK FAQ: http://www.async.com.br/faq/pygtk/

Reply via email to