[cells-gtk-devel] Re: Multithreading patch
Peter Hildebrandt
peter.hildebrandt at gmail.com
Tue Dec 11 15:51:55 UTC 2007
Here it is, finally. Multithreading for cells-gtk on sbcl/linux.
http://www.washbear-network.de/peterblog/wp-content/uploads/2007/12/cells-gtk-threading-2.patch
(since it's too big for the list, I revived my blog and put stuff there.
Check it out if you like: <http://www.washbear-network.de/peterblog>)
I built the patch against the current cvs (which is different from the
tar.gz linked on the website, which is different from the asdf-install
version). Applications will work just as they used to (a littler better,
hopefully, see below).
To run an application in the background, just use the new
(start-win 'app-class initargs)
which starts your application window in the background and returns right
to the repl. The return value is the freshly created instance of
app-class. That's it.
To initialize cells-gtk, there is the new (init-gtk). It is threadsafe
and makes sure that no important state is destroyed. That is, you can
call it while other windows are open in the background. Don't do this
with cells-gtk-init. You will need to call init-gtk before you do
anything to cells-gtk (it loads foreign libraries). start-win takes care
of that for you.
A couple details:
- To simulate the idea of a "main window" (thanks Kenny for pointing out
that this is not something "natural") gtk-app now has the slot
terminate-on-exit -- if this is set to true, all open windows will be
closed when this is closed.
- (open-windows) gives you a list of, well, open windows
- You can close all windows by (close-all-windows) or (init-gtk
:close-all-windows t)
- Calls from the repl can be wrapped in (with-gdk-threads ...), in theory
this is the right thing to do. In practice it mostly works if you don't.
However, every now and then I broke my gdk this way and had to restart
lisp. In a real app, always wrap calls that might affect your window in
(with-gdk-threads ...).
Multithreading and gtk is tricky. Calls have to wrapped in the macro
*exactly once* -- do it more than once, and you get a deadlock. Do it
less than once, and gtk might fail with an Xlib asynchronity.
For now, wrap everything at the highest level in with-gdk-threads, except
for start-win.
- You can start the gtk thread manually by calling (start-gtk-main).
start-win does that for you.
- The gtk thread can be terminated by calling (stop-gtk-main). In order
to use gtk again, you will have to restart lisp. Normally this is utterly
useless. You might want it if you ship your app as a save-lisp-and-die
image. Maybe.
Here's a silly example of interactive UI development:
;; make a package
(defpackage :cgtk-user (:use :cl :cgtk :cells))
(in-package :cgtk-user)
;; start a blank app
(start-win 'gtk-app :title (c-in "My App") :kids (c-in nil)) ; it appears
;; bind it to a variable
(defparameter *app* *)
;; change its title
(setf (title *app*) "Application") ; the title changes
;; create a vbox to take the our widgets
(push (mk-vbox :kids (c-in nil)) (kids *app*))
(defparameter *vbox* (first *))
;; make a button
(push (mk-button :label "Click me" :on-clicked (callback (w e d) (print
'you-clicked-me))) (kids *vbox*)) ; it shows up
;; make a counter
(push (mk-button :label (c-in "Me too") :on-clicked (let ((count 0))
(callback (w e d) (print (incf count))))) (kids *vbox*))
;; relabel that button
(setf (label (first *)) "Counter")
;; close all windows
(close-all-windows)
That's all I can think of right now. Let me if clarifications are needed.
Regards,
Peter
Some technical details:
- The way gtk-app used to handle restarts resulted in effect in a mutual
recursion: Closing a window resulted in signaling quit, which led to
transfer of control out of gtk-main into lisp, which then exited, leaving
the gtk-main task waiting for a return (that would never come). Each time
you ran an application, that stack would build up.
The previous way to unwind that stack upon exit, like
(loop for i below (gtk-main-level) do (gtk-main-quit)) ;; I had this
one somehow
or
(loop while (> (gtk-main-level) 0) do (gtk-main-quit)) ;; from cvs,
results for me in endless loop, since gtk-main-level won't reduce for me
by calling gtk-main-quit like this
This does not do the trick, since gtk-main-quit only works as expected for
active gtk-main loops, not for stale ones (which you get after
transferring control out of them by means of a signal or error). In
effect, I found no way to resume an open instance of gtk-main once a lisp
callback has used error or signal to uncleanly jump out of it. I believe
that this could be the reason why the termination code in gtk-app.lisp
needs several workarounds for different lisps: something's not kosher
here.
I rewrote gtk-app completely so that now in effect one instance of
gtk-main runs continously. When an application with :terminate-on-close
is closed, gtk-main-quit is invoked while gtk-main is still running,
gtk-main thus cleanly left. Through gtk-quit callbacks open windows are
closed, and gtk-main is called again. This way no call stack piles up.
In the normal case there should be no callstack building up this way. You
can check with (gtk-main-level).
Only errors in callbacks still lead to recursive re-entries. Therefore I
left the current termination code in place, though my changes should make
this the exception rather than the normal case.
If someone figures out how to re-enter gtk-main after a callback signaled
an error, I'd be very excited to learn.
- I added an additional error handler that catches lisp errors. Thus, if
a callback raises a lisp error, this is caught within the thread running
gtk-main and a message box is displayed. The user has the option to
resignal the error (results in invoking the debugger) or to "recklessly
continue" (which works fine as long as the callback has not caused
inconsistent state). This is helpful if (like me) you deal with user
input and you prototype does not handle typos well
- cells-gtk has a major leak due to the nature it synchronized clos with
gtk objects: Upon creation, each clos object in cells-gtk is registered
in *gtk-objects* along with the pointer to its gtk equivalent. However,
this entry is not removed when the gtk-instance is freed. When
subsequently new objects are created, gtk may well reuse this space,
causing cells-gtk to signal an error like "new object has pointer x, but
this is already known as such-and-such". Curiously, it is usually only
*gtk-objects* which holds a reference to an obsolete object. For
instance, running test-gtk allocates some 330 objects in *gtk-objects*,
about 100 of which are not freed, when the app is closed.
The right way to solve this would be to hook into the on-delete-event
signal of the widget object -- unfortunately it seems this is not called
every time gtk deletes a widget. I introduced some additional cleanup
code in cells-gtk, thus covering about 70% of leftover widgets.
For the rest, I believe the right thing is to trust gtk when in doubt --
thus I modified the gtk-objects registry to forget about the old object if
gtk has given the pointer to a new one. (It used to throw an error and
break gtk. I had it signal a warning in the beginning, but I came to
believe that this cannot be the right way to deal with it; it would entail
writing C-style memory management, carefully keeping track of objects and
manually freeing them. Ewww.)
This hasn't been a problem so far since it seems most cells-gtk apps just
call cells-gtk-init before launching their app, thus wiping *gtk-objects*
(and along with that all gtk state). Not exactly dynamic ;).
For an example where the problem arises, consider the following slot def:
(caption-widgets :initform (c? (loop for caption in captions collecting
(mk-label :text caption))))
Once you got to somehow free all the old labels everytime you create new
ones, things get -- ehh -- ugly.
It looks like what we see here is the due penalty for using a C-library
for doing our window management. Maybe one day CLIM will be fast and
sleek.
- For every new window, upon creation a gtk-quit callback is registered,
closing that window once gtk-main-quit is called from within a callback.
You force this to happen by setting terminate-on-close to t in a gtk-app
window.
- I moved some initialization from start-app to an own function,
init-gtk. This one also takes care of multiple initialization.
- To prevent double initialization of glib's threading faacility via
g-thread-init, there is a global variable in gtk-app.lisp. This works
fine, until you recompile this file. So I figured why keep track of state
when glib can do this for us? It turns out there is a macro in glib for
this purpose. Since it's a macro (not a function), I added a respective
function to gtk-adds.c. A dependency on glib-2.0 is introduced, since
threading is a glib feature, not gtk (this should be uncritical -- linux
systems without glib are -- errr -- rare). I added the corresponding
stuff to Makefile. Thus, when you compile cells-gtk with #+libcellsgtk,
it will use this function. Otherwise there is a flag variable in a
closure around (init-gtk).
- On the same note, init-gtk checks whether the libs have beeen loaded by
calling a function (gtk-main-level) from within a handler-case. If this
fails, we know they haven't been loaded. I prefer to tap into exiting
state information over duplicating it.
- The threading code is currently SBCL specific. However, I singled out
the needed primitives in four macro definitions, therefore it should be
easy to add the respective calls for other implementations:
(defmacro thread-current ()
#+sbcl `(identity sb-thread:*current-thread*)
)
(defmacro thread-make (thread-fn name)
(declare (ignorable name))
#+sbcl `(sb-thread:make-thread ,thread-fn :name ,name)
)
(defmacro thread-kill (thread)
#+sbcl `(sb-thread:terminate-thread ,thread)
)
(defmacro thread-yield ()
#+sbcl `(sb-thread:thread-yield)
)
- My changes will compile and run just fine without sbcl. Everything
should work normally, you just won't be able to use the threading
functions like start-win and close-all-windows.
More information about the cells-gtk-devel
mailing list