* thread cancellation, take 2 @ 2007-09-20 14:30 Julian Graham 2007-09-20 15:18 ` Ludovic Courtès 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-09-20 14:30 UTC (permalink / raw) To: guile-devel Hi Guilers, A while back I did some work on introducing a thread cancellation API into Guile's thread code. At the time, the thread refactor for 1.8 hadn't yet happened, and I didn't really know enough about Guile internals to get anything working. I came back to my code recently and made another attempt to integrate it, and I STILL couldn't make it work, but now at least I think I know the right questions to ask. What I had in mind for thread cancellation is a set of three functions that mirrors the pthreads API: cancel-thread, push-thread-cleanup, and pop-thread-cleanup (as with pthreads, push-thread-cleanup and pop-thread-cleanup respectively add and remove expressions to and from a stack that is evaluated at the time cancel-thread is called). My implementation involves installing a real pthreads cancellation handler that manages evaluation of Scheme cleanup handlers and some thread shutdown behavior (although most of this is handled by the new on_thread_exit stuff). The problem I'm having comes, I think, when I try to evaluate the items on the cleanup list -- because the cancellation handler stack is the result of a non-local exit from Guile's normal execution, I think there's something wrong with the state of the stack boundaries or registers that makes the GC freak out (heap corruption, I think, leading to a double free() somewhere down the road) when I do an eval in that context (whether or not I'm in Guile mode). So: Is there a way to safely evaluate SCMs from C after a JMP into a weird context? (I imagine the on_thread_exit code is called in a similar state, but it only superficially manipulates SCM values...) I've tried explicitly resetting the top and base pointers of the stack, as well as updating the register window, but that didn't seem to help. (At the time I asked about this originally, Marius Vollmer suggested that cancellation be implemented as a system-async -- I think that approach leaves us with the same issues regarding the GC / stack, especially now that Guile threads are isomorphic to pthreads.) On a related note, do you guys have any preferences as to the behavior of cancel-thread? The way I've got it now, joining threads will receive the value of the final expression in the cleanup-handler list as the "result" of the canceled thread. Does that make sense? Also, should the items in the cleanup-handlers list be S-exprs or should I limit them to being thunks (kind of leaning toward the latter right now...)? Thanks, Julian P.S. Here's a link to the thread from when I first tried to do this: http://lists.gnu.org/archive/html/guile-devel/2004-10/msg00075.html _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-20 14:30 thread cancellation, take 2 Julian Graham @ 2007-09-20 15:18 ` Ludovic Courtès 2007-09-20 15:36 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-09-20 15:18 UTC (permalink / raw) To: Julian Graham; +Cc: guile-devel Hi, "Julian Graham" <joolean@gmail.com> writes: > So: Is there a way to safely evaluate SCMs from C after a JMP into a > weird context? (I imagine the on_thread_exit code is called in a > similar state, but it only superficially manipulates SCM values...) Would it be possible to defer execution of the Scheme code (cleanup handlers) to after the C cleanup handler has been called? I.e., the C handler would push the Scheme handlers to a list that would be eventually executed, when Guile is back into a "clean", regular state. > (At the time I asked about this originally, Marius Vollmer suggested > that cancellation be implemented as a system-async -- I think that > approach leaves us with the same issues regarding the GC / stack, > especially now that Guile threads are isomorphic to pthreads.) Indeed, some form of deferred execution appears to be needed here, so that Scheme code is not evaluated right from the C cleanup handler. Now, if system asyncs can do the job, then it's better to use them rather than some ad hoc mechanism as I suggested above. > On a related note, do you guys have any preferences as to the > behavior of cancel-thread? The way I've got it now, joining threads > will receive the value of the final expression in the cleanup-handler > list as the "result" of the canceled thread. Does that make sense? Sounds good. > Also, should the items in the cleanup-handlers list be S-exprs or > should I limit them to being thunks (kind of leaning toward the latter > right now...)? I'd prefer thunks as well, it looks more Schemey. Hope this helps, Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-20 15:18 ` Ludovic Courtès @ 2007-09-20 15:36 ` Julian Graham 2007-09-23 5:16 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-09-20 15:36 UTC (permalink / raw) To: Ludovic Courtès, guile-devel > Would it be possible to defer execution of the Scheme code (cleanup > handlers) to after the C cleanup handler has been called? > > I.e., the C handler would push the Scheme handlers to a list that would > be eventually executed, when Guile is back into a "clean", regular > state. Yeah, that would be fine -- except that after the thread is signaled for cancellation, it'll never go back into a regular state. That is, I think the next thing that happens is that the thread-local exit handlers (on_thread_exit in threads.c) get called -- and I don't think that code evaluates queued asyncs (please let me know if it actually does!). And any queued asyncs would have to be evaluated within that thread (and in order), because people are going to want to do things that have to be done from within a particular thread, like unlock mutexes. > I'd prefer thunks as well, it looks more Schemey. I agree -- and makes things way easier if it turns out it's possible to do this with asyncs. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-20 15:36 ` Julian Graham @ 2007-09-23 5:16 ` Julian Graham 2007-09-23 10:42 ` Ludovic Courtès 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-09-23 5:16 UTC (permalink / raw) To: Ludovic Courtès, guile-devel [-- Attachment #1: Type: text/plain, Size: 1857 bytes --] Alright -- I think I've got it working. After mucking about for a bit with asyncs, I realized that it makes more sense to evaluate cleanup handlers in do_thread_exit -- and evaluation should happen there anyway, since pthreads says cleanup handlers get called when the thread exits for any reason, not just cancellation. Duh. The patch I've attached adds three new public functions: cancel-thread, push-thread-cleanup, and pop-thread-cleanup. API documentation on their behavior is included in the changes. I've never submitted a patch for Guile before, so I've likely made a few mistakes in formatting, etc., and I don't really know the vetting procedure. I hope I've gotten the main bits right, though. Please let me know if there are any changes I should make. On 9/20/07, Julian Graham <joolean@gmail.com> wrote: > > Would it be possible to defer execution of the Scheme code (cleanup > > handlers) to after the C cleanup handler has been called? > > > > I.e., the C handler would push the Scheme handlers to a list that would > > be eventually executed, when Guile is back into a "clean", regular > > state. > > Yeah, that would be fine -- except that after the thread is signaled > for cancellation, it'll never go back into a regular state. That is, > I think the next thing that happens is that the thread-local exit > handlers (on_thread_exit in threads.c) get called -- and I don't think > that code evaluates queued asyncs (please let me know if it actually > does!). And any queued asyncs would have to be evaluated within that > thread (and in order), because people are going to want to do things > that have to be done from within a particular thread, like unlock > mutexes. > > > I'd prefer thunks as well, it looks more Schemey. > > I agree -- and makes things way easier if it turns out it's possible > to do this with asyncs. > [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: thread-cancellation.HEAD.patch --] [-- Type: text/x-patch; name="thread-cancellation.HEAD.patch", Size: 6708 bytes --] ? thread-cancellation.HEAD.patch Index: null-threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/null-threads.h,v retrieving revision 1.12 diff -u -r1.12 null-threads.h --- null-threads.h 17 Apr 2006 00:05:40 -0000 1.12 +++ null-threads.h 23 Sep 2007 04:57:27 -0000 @@ -41,6 +41,7 @@ #define scm_i_pthread_create(t,a,f,d) (*(t)=0, (void)(f), ENOSYS) #define scm_i_pthread_detach(t) do { } while (0) #define scm_i_pthread_exit(v) exit(0) +#define scm_i_pthread_cancel(t) 0 #define scm_i_sched_yield() 0 /* Signals Index: pthread-threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/pthread-threads.h,v retrieving revision 1.15 diff -u -r1.15 pthread-threads.h --- pthread-threads.h 9 Oct 2006 23:21:00 -0000 1.15 +++ pthread-threads.h 23 Sep 2007 04:57:27 -0000 @@ -35,6 +35,7 @@ #define scm_i_pthread_create pthread_create #define scm_i_pthread_detach pthread_detach #define scm_i_pthread_exit pthread_exit +#define scm_i_pthread_cancel pthread_cancel #define scm_i_sched_yield sched_yield /* Signals Index: threads.c =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/threads.c,v retrieving revision 1.88 diff -u -r1.88 threads.c --- threads.c 15 Jan 2007 23:35:34 -0000 1.88 +++ threads.c 23 Sep 2007 04:57:31 -0000 @@ -131,6 +131,7 @@ { scm_i_thread *t = SCM_I_THREAD_DATA (obj); scm_gc_mark (t->result); + scm_gc_mark (t->cleanup_handlers); scm_gc_mark (t->join_queue); scm_gc_mark (t->dynwinds); scm_gc_mark (t->active_asyncs); @@ -415,6 +416,7 @@ t->pthread = scm_i_pthread_self (); t->handle = SCM_BOOL_F; t->result = SCM_BOOL_F; + t->cleanup_handlers = SCM_EOL; t->join_queue = SCM_EOL; t->dynamic_state = SCM_BOOL_F; t->dynwinds = SCM_EOL; @@ -434,6 +436,7 @@ scm_i_pthread_mutex_init (&t->heap_mutex, NULL); t->clear_freelists_p = 0; t->gc_running_p = 0; + t->canceled = 0; t->exited = 0; t->freelist = SCM_EOL; @@ -473,12 +476,32 @@ t->block_asyncs = 0; } +static SCM handle_cleanup_handler(void *cont, SCM tag, SCM args) { + *((int *) cont) = 0; + return scm_handle_by_message_noexit(NULL, tag, args); + return SCM_UNDEFINED; +} + /* Perform thread tear-down, in guile mode. */ static void * do_thread_exit (void *v) { - scm_i_thread *t = (scm_i_thread *)v; + scm_i_thread *t = (scm_i_thread *) v; + + while(!scm_is_eq(t->cleanup_handlers, SCM_EOL)) + { + int cont = 1; + SCM ptr = SCM_CAR(t->cleanup_handlers); + t->cleanup_handlers = SCM_CDR(t->cleanup_handlers); + t->result = scm_internal_catch (SCM_BOOL_T, + (scm_t_catch_body) scm_call_0, ptr, + handle_cleanup_handler, &cont); + if (!cont) + { + break; + } + } scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); @@ -489,6 +512,7 @@ ; scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return NULL; } @@ -882,6 +906,78 @@ } #undef FUNC_NAME +SCM_DEFINE (scm_cancel_thread, "cancel-thread", 1, 0, 0, + (SCM thread), +"Asynchronously force the target @var{thread} to terminate. @var{thread} " +"cannot be the current thread, and if @var{thread} has already terminated or " +"been signaled to terminate, this function is a no-op.") +#define FUNC_NAME s_scm_cancel_thread +{ + scm_i_thread *t = NULL; + + SCM_VALIDATE_THREAD (1, thread); + t = SCM_I_THREAD_DATA (thread); + if (t == SCM_I_CURRENT_THREAD) + { + SCM_MISC_ERROR ("cannot cancel the current thread", SCM_EOL); + } + scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); + if (!t->canceled) + { + t->canceled = 1; + scm_i_pthread_cancel(t->pthread); + } + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return SCM_UNDEFINED; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_push_thread_cleanup, "push-thread-cleanup", 1, 0, 0, + (SCM proc), +"Add the thunk @var{proc} to the front of the list of cleanup handlers for " +"the current thread. These handlers will be called in a LIFO manner when the " +"current thread exits.") +#define FUNC_NAME s_scm_push_thread_cleanup +{ + scm_i_thread *thread = SCM_I_CURRENT_THREAD; + if (!scm_is_eq (scm_thunk_p (proc), SCM_BOOL_T)) + { + SCM_MISC_ERROR ("proc must be a thunk", SCM_EOL); + } + scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); + thread->cleanup_handlers = scm_cons (proc, thread->cleanup_handlers); + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return SCM_BOOL_T; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_pop_thread_cleanup, "pop-thread-cleanup", 0, 1, 0, + (SCM evalp), +"Remove the most recently added cleanup handler from the current thread's " +"list of cleanup handlers. If @car{evalp} is specified and evaluates to " +"true, the cleanup handler will be called as it is removed.") +#define FUNC_NAME s_scm_pop_thread_cleanup +{ + scm_i_thread *thread = SCM_I_CURRENT_THREAD; + scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); + if (!scm_is_eq (thread->cleanup_handlers, SCM_EOL)) + { + SCM ret = SCM_BOOL_T; + SCM ptr = SCM_CAR (thread->cleanup_handlers); + thread->cleanup_handlers = SCM_CDR (thread->cleanup_handlers); + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + if (!scm_is_eq (evalp, SCM_BOOL_F)) + { + ret = scm_call_0 (ptr); + } + thread->cleanup_handlers = SCM_CDR (thread->cleanup_handlers); + return ret; + } + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return SCM_BOOL_F; +} +#undef FUNC_NAME + SCM_DEFINE (scm_join_thread, "join-thread", 1, 0, 0, (SCM thread), "Suspend execution of the calling thread until the target @var{thread} " Index: threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/threads.h,v retrieving revision 1.48 diff -u -r1.48 threads.h --- threads.h 17 Apr 2006 00:05:42 -0000 1.48 +++ threads.h 23 Sep 2007 04:57:31 -0000 @@ -49,9 +49,11 @@ SCM handle; scm_i_pthread_t pthread; - + + SCM cleanup_handlers; SCM join_queue; SCM result; + int canceled; int exited; SCM sleep_object; @@ -153,6 +155,9 @@ SCM_API SCM scm_call_with_new_thread (SCM thunk, SCM handler); SCM_API SCM scm_yield (void); +SCM_API SCM scm_cancel_thread (SCM t); +SCM_API SCM scm_push_thread_cleanup (SCM thunk); +SCM_API SCM scm_pop_thread_cleanup (SCM evalp); SCM_API SCM scm_join_thread (SCM t); SCM_API SCM scm_make_mutex (void); [-- Attachment #3: Type: text/plain, Size: 143 bytes --] _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-23 5:16 ` Julian Graham @ 2007-09-23 10:42 ` Ludovic Courtès 2007-09-23 18:39 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-09-23 10:42 UTC (permalink / raw) To: guile-devel Hi Julian, "Julian Graham" <joolean@gmail.com> writes: > Alright -- I think I've got it working. After mucking about for a bit > with asyncs, I realized that it makes more sense to evaluate cleanup > handlers in do_thread_exit -- and evaluation should happen there > anyway, since pthreads says cleanup handlers get called when the > thread exits for any reason, not just cancellation. Duh. Good! > The patch I've attached adds three new public functions: > cancel-thread, push-thread-cleanup, and pop-thread-cleanup. API > documentation on their behavior is included in the changes. Thinking a bit more about it, maybe we could let users handle the list of handlers if needed. That is, we'd have just `set-thread-cleanup-procedure!' (a two-argument procedure whose first arg is a thread) and `thread-cleanup-procedure' (a one-argument procedure); users who have cleanup code spread over several procedure would provide a procedure that iterates over such pieces of code. That would keep Guile's built-in mechanisms minimal. What do you think? > I've never submitted a patch for Guile before, so I've likely made a > few mistakes in formatting, etc., and I don't really know the vetting > procedure. I hope I've gotten the main bits right, though. Please let > me know if there are any changes I should make. First, you'll need to assign copyright to the FSF so that we can incorporate your changes (I'll send you the relevant stuff off-line). Then, you need to make sure your code follows the GNU Standards as much as possible (a few comments follow). Also, please add a few test cases to `threads.test' that exercise the new API. I think the patch adds a useful feature so, unless someone raises an objection, I'd be OK to apply it to HEAD when you're done with the copyright paperwork. > +static SCM handle_cleanup_handler(void *cont, SCM tag, SCM args) { > + *((int *) cont) = 0; > + return scm_handle_by_message_noexit(NULL, tag, args); > + return SCM_UNDEFINED; > +} The second `return' is superfluous. Also, for clarity and consistency with the GCS, the function should rather read: static SCM cleanup_handler_error_handler (void *cont, SCM tag, SCM args) { int *continue_p = (int *) cont; *continue_p = 0; return scm_handle_by_message_noexit (NULL, tag, args); } But if you agree with the change I suggested above (removing the list of handlers), you no longer need that function: you can just use `scm_handle_by_message_noexit ()' directly. > + while(!scm_is_eq(t->cleanup_handlers, SCM_EOL)) > + { > + int cont = 1; > + SCM ptr = SCM_CAR(t->cleanup_handlers); > + t->cleanup_handlers = SCM_CDR(t->cleanup_handlers); Please add a whitespace before opening parentheses. > +SCM_DEFINE (scm_cancel_thread, "cancel-thread", 1, 0, 0, > + (SCM thread), > +"Asynchronously force the target @var{thread} to terminate. @var{thread} " > +"cannot be the current thread, and if @var{thread} has already terminated or " > +"been signaled to terminate, this function is a no-op.") Better indent the doc string. Thanks, Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-23 10:42 ` Ludovic Courtès @ 2007-09-23 18:39 ` Julian Graham 2007-09-24 11:42 ` Ludovic Courtès 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-09-23 18:39 UTC (permalink / raw) To: Ludovic Courtès; +Cc: guile-devel > Thinking a bit more about it, maybe we could let users handle the list > of handlers if needed. That is, we'd have just > `set-thread-cleanup-procedure!' (a two-argument procedure whose first > arg is a thread) and `thread-cleanup-procedure' (a one-argument > procedure); users who have cleanup code spread over several procedure > would provide a procedure that iterates over such pieces of code. That > would keep Guile's built-in mechanisms minimal. > > What do you think? Well, in the use case for the native pthread_cancel implementation, it makes a lot of sense to treat the handler list as a stack. That is, you have something like: [LOCK MUTEX 1] pthread_cleanup_push([UNLOCK MUTEX 1]) [ ...CODE CONTAINING CANCELLATION POINTS... ] [LOCK MUTEX 2] pthread_cleanup_push([UNLOCK MUTEX 2]) [ ... ] pthread_cleanup_pop() [UNLOCK MUTEX 2] [ ... ] pthread_cleanup_pop() [UNLOCK MUTEX 1] What's useful about this is that you can have several resources at different levels of nesting that need to be cleaned up at cancellation time, but you only need to worry about pushing and popping handlers within your own scope. If people feel that managing the entire list at once is more Scheme-y, though, I can change it. > First, you'll need to assign copyright to the FSF so that we can > incorporate your changes (I'll send you the relevant stuff off-line). > Then, you need to make sure your code follows the GNU Standards as much > as possible (a few comments follow). Also, please add a few test cases > to `threads.test' that exercise the new API. Great -- I'll deal with the paperwork, add tests, and clean up the code as you've suggested. I'll send the revisions to you offline, if that's okay. Thanks, Julian _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-23 18:39 ` Julian Graham @ 2007-09-24 11:42 ` Ludovic Courtès 2007-09-24 15:39 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-09-24 11:42 UTC (permalink / raw) To: Julian Graham; +Cc: guile-devel Hi, "Julian Graham" <joolean@gmail.com> writes: > What's useful about this is that you can have several resources at > different levels of nesting that need to be cleaned up at cancellation > time, but you only need to worry about pushing and popping handlers > within your own scope. If people feel that managing the entire list > at once is more Scheme-y, though, I can change it. I find it more elegant to use closures to that end. I.e., when installing a handler, you'd write something like this: (let ((cleanup (thread-cleanup-procedure (current-thread)))) (set-thread-cleanup-procedure! (current-thread) (lambda () ;; invoke previous handler (if (procedure? cleanup) (cleanup)) ;; clean up... ))) There's a race here in case multiple threads try to change the cleanup procedure associated with that particular thread at the same time, but I suppose it is not an issue in practice. > Great -- I'll deal with the paperwork, add tests, and clean up the > code as you've suggested. I'll send the revisions to you offline, if > that's okay. Please post updated patches on the list as well so others get a chance to review them. Thanks! Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-24 11:42 ` Ludovic Courtès @ 2007-09-24 15:39 ` Julian Graham 2007-09-24 20:17 ` Julian Graham 2007-09-26 4:03 ` Ludovic Courtès 0 siblings, 2 replies; 23+ messages in thread From: Julian Graham @ 2007-09-24 15:39 UTC (permalink / raw) To: Ludovic Courtès, guile-devel > I find it more elegant to use closures to that end. I.e., when > installing a handler, you'd write something like this: > > (let ((cleanup (thread-cleanup-procedure (current-thread)))) > (set-thread-cleanup-procedure! (current-thread) > (lambda () > ;; invoke previous handler > (if (procedure? cleanup) > (cleanup)) > ;; clean up... > ))) > > There's a race here in case multiple threads try to change the cleanup > procedure associated with that particular thread at the same time, but I > suppose it is not an issue in practice. Fair enough, re: closures. But why should callers outside the current thread be able to access that thread's cleanup handler procedure? Maybe this isn't a realistic issue, but you could use this to "inject" arbitrary code into a separate thread by setting the cleanup procedure and immediately canceling the thread. Why not treat the handler as thread-specific data? _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-24 15:39 ` Julian Graham @ 2007-09-24 20:17 ` Julian Graham 2007-09-26 4:03 ` Ludovic Courtès 1 sibling, 0 replies; 23+ messages in thread From: Julian Graham @ 2007-09-24 20:17 UTC (permalink / raw) To: guile-devel On a related note, if it's okay with everyone, I'd like to add a thread-exit function in the next revision of this patch -- we're just so close to SRFI-18 compatibility, and that would take us the last few steps, I think. Unfortunately, it doesn't seem like calling pthread_exit doesn't seem to do the trick, at least not in the main REPL thread -- it puts the process into a zombie state. It works in threads spawned by the REPL, and it works when you call it in such a way that the REPL doesn't get started up (e.g., guile -c "(exit-thread)"). I suspect this has everything to do with the signal delivery thread, but I'm struggling with the rest. Why would the fact that the signal delivery thread is left listening on a closed pipe make the process into a zombie? Exiting the entire process seems to work, so I could check to see whether the calling thread is the only active thread and then call exit(thread->result) if it is, but that's rather inelegant. (Does it even make sense to let people exit the REPL thread?) Maybe we should mark certain special threads internally as being, for example, the REPL or the signal delivery thread, etc. Can someone shed some light? I feel stupid. On 9/24/07, Julian Graham <joolean@gmail.com> wrote: > > I find it more elegant to use closures to that end. I.e., when > > installing a handler, you'd write something like this: > > > > (let ((cleanup (thread-cleanup-procedure (current-thread)))) > > (set-thread-cleanup-procedure! (current-thread) > > (lambda () > > ;; invoke previous handler > > (if (procedure? cleanup) > > (cleanup)) > > ;; clean up... > > ))) > > > > There's a race here in case multiple threads try to change the cleanup > > procedure associated with that particular thread at the same time, but I > > suppose it is not an issue in practice. > > Fair enough, re: closures. But why should callers outside the current > thread be able to access that thread's cleanup handler procedure? > Maybe this isn't a realistic issue, but you could use this to "inject" > arbitrary code into a separate thread by setting the cleanup procedure > and immediately canceling the thread. Why not treat the handler as > thread-specific data? _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-24 15:39 ` Julian Graham 2007-09-24 20:17 ` Julian Graham @ 2007-09-26 4:03 ` Ludovic Courtès 2007-09-27 2:39 ` Julian Graham 1 sibling, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-09-26 4:03 UTC (permalink / raw) To: Julian Graham; +Cc: guile-devel Hi, "Julian Graham" <joolean@gmail.com> writes: > Fair enough, re: closures. But why should callers outside the current > thread be able to access that thread's cleanup handler procedure? > Maybe this isn't a realistic issue, but you could use this to "inject" > arbitrary code into a separate thread by setting the cleanup procedure > and immediately canceling the thread. Why not treat the handler as > thread-specific data? Anyway, as long as you have a reference to an object (including a thread object), you can use the relevant procedures to mutate it. For instance, if my code passes a list to yours, I have no guarantee that your code won't call `set-car!' on it. Just to say that hiding the data doesn't solve this authorization problem, it just makes it less visible. Beside, it may be useful in some cases to assign a thread finalizer from outside the thread itself. Thanks, Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-26 4:03 ` Ludovic Courtès @ 2007-09-27 2:39 ` Julian Graham 2007-10-18 0:41 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-09-27 2:39 UTC (permalink / raw) To: Ludovic Courtès; +Cc: guile-devel [-- Attachment #1: Type: text/plain, Size: 1588 bytes --] On 9/26/07, Ludovic Courtès <ludovic.courtes@laas.fr> wrote: > Anyway, as long as you have a reference to an object (including a thread > object), you can use the relevant procedures to mutate it. For > instance, if my code passes a list to yours, I have no guarantee that > your code won't call `set-car!' on it. > > Just to say that hiding the data doesn't solve this authorization > problem, it just makes it less visible. > > Beside, it may be useful in some cases to assign a thread finalizer from > outside the thread itself. Okay. Here's the latest revision of the patch: * Changed the API as Ludovic suggests above -- push-thread-cleanup and pop-thread-cleanup are now, respectively, set-thread-cleanup! and thread-cleanup, and you can set and get to / from whatever thread you want. * Updated threads.test. * Modified the behavior of cancel-thread to allow for canceling the current thread. This necessitated some changes to scmsigs, so that the signal delivery thread could be notified to shut down -- not cleanly closing the signal pipe was leaving the process in a zombie state -- specifically, the addition of an extra mutex and a function in scmsigs.c. Let me know if this is too sloppy; rationale is provided in the comments. * Also (I realize this is a bit unrelated) changed the behavior of all-threads so that it doesn't include the signal delivery thread, because nobody has any business messing with it. Exposed a pointer to the delivery thread from of scmsigs.o. I hope this isn't too sloppy, either. Regards, Julian [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: thread-cancellation.HEAD.patch --] [-- Type: text/x-patch; name="thread-cancellation.HEAD.patch", Size: 12837 bytes --] Index: libguile/null-threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/null-threads.h,v retrieving revision 1.12 diff -a -u -r1.12 null-threads.h --- libguile/null-threads.h 17 Apr 2006 00:05:40 -0000 1.12 +++ libguile/null-threads.h 27 Sep 2007 02:20:32 -0000 @@ -41,6 +41,9 @@ #define scm_i_pthread_create(t,a,f,d) (*(t)=0, (void)(f), ENOSYS) #define scm_i_pthread_detach(t) do { } while (0) #define scm_i_pthread_exit(v) exit(0) +#define scm_i_pthread_cancel(t) 0 +#define scm_i_pthread_cleanup_push(t,v) 0 +#define scm_i_pthread_cleanup_pop(e) 0 #define scm_i_sched_yield() 0 /* Signals Index: libguile/pthread-threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/pthread-threads.h,v retrieving revision 1.15 diff -a -u -r1.15 pthread-threads.h --- libguile/pthread-threads.h 9 Oct 2006 23:21:00 -0000 1.15 +++ libguile/pthread-threads.h 27 Sep 2007 02:20:32 -0000 @@ -35,6 +35,9 @@ #define scm_i_pthread_create pthread_create #define scm_i_pthread_detach pthread_detach #define scm_i_pthread_exit pthread_exit +#define scm_i_pthread_cancel pthread_cancel +#define scm_i_pthread_cleanup_push pthread_cleanup_push +#define scm_i_pthread_cleanup_pop pthread_cleanup_pop #define scm_i_sched_yield sched_yield /* Signals Index: libguile/scmsigs.c =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/scmsigs.c,v retrieving revision 1.97 diff -a -u -r1.97 scmsigs.c --- libguile/scmsigs.c 7 Mar 2007 23:10:52 -0000 1.97 +++ libguile/scmsigs.c 27 Sep 2007 02:20:33 -0000 @@ -33,6 +33,7 @@ #include "libguile/eval.h" #include "libguile/root.h" #include "libguile/vectors.h" +#include "libguile/threads.h" #include "libguile/validate.h" #include "libguile/scmsigs.h" @@ -99,6 +100,9 @@ static SCM signal_handler_asyncs; static SCM signal_handler_threads; +scm_i_thread *scm_i_signal_delivery_thread; +static scm_i_pthread_mutex_t signal_delivery_thread_mutex; + /* saves the original C handlers, when a new handler is installed. set to SIG_ERR if the original handler is installed. */ #ifdef HAVE_SIGACTION @@ -185,20 +189,25 @@ if (scm_is_true (h)) scm_system_async_mark_for_thread (h, t); } + else if (n == 0) + break; /* the signal pipe was closed. */ else if (n < 0 && errno != EINTR) perror ("error in signal delivery thread"); } - return SCM_UNSPECIFIED; /* not reached */ + return SCM_UNSPECIFIED; /* not reached unless all other threads exited */ } static void start_signal_delivery_thread (void) { + scm_i_pthread_mutex_lock (&signal_delivery_thread_mutex); if (pipe (signal_pipe) != 0) scm_syserror (NULL); - scm_spawn_thread (signal_delivery_thread, NULL, - scm_handle_by_message, "signal delivery thread"); + scm_i_signal_delivery_thread = SCM_I_THREAD_DATA + (scm_spawn_thread (signal_delivery_thread, NULL, + scm_handle_by_message, "signal delivery thread")); + scm_i_pthread_mutex_unlock (&signal_delivery_thread_mutex); } static void @@ -653,10 +662,29 @@ \f void +scm_i_close_signal_pipe() +{ + /* signal_delivery_thread_mutex will only be locked while the signal + delivery thread is being launched. The thread that calls this function + is already holding the thread admin mutex, so if the delivery thread + hasn't been launched at this point, it never will be before shutdown. + */ + scm_i_pthread_mutex_lock (&signal_delivery_thread_mutex); + if (scm_i_signal_delivery_thread != NULL) + { + close (signal_pipe[1]); + } + scm_i_pthread_mutex_unlock (&signal_delivery_thread_mutex); +} + +void scm_init_scmsigs () { int i; + scm_i_pthread_mutex_init (&signal_delivery_thread_mutex, NULL); + scm_i_signal_delivery_thread = NULL; + signal_handlers = SCM_VARIABLE_LOC (scm_c_define ("signal-handlers", scm_c_make_vector (NSIG, SCM_BOOL_F))); Index: libguile/scmsigs.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/scmsigs.h,v retrieving revision 1.17 diff -a -u -r1.17 scmsigs.h --- libguile/scmsigs.h 17 Apr 2006 00:05:40 -0000 1.17 +++ libguile/scmsigs.h 27 Sep 2007 02:20:33 -0000 @@ -41,6 +41,8 @@ SCM_API SCM scm_raise (SCM sig); SCM_API void scm_init_scmsigs (void); +SCM_API void scm_i_close_signal_pipe (void); + #endif /* SCM_SCMSIGS_H */ /* Index: libguile/threads.c =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/threads.c,v retrieving revision 1.88 diff -a -u -r1.88 threads.c --- libguile/threads.c 15 Jan 2007 23:35:34 -0000 1.88 +++ libguile/threads.c 27 Sep 2007 02:20:34 -0000 @@ -48,6 +48,7 @@ #include "libguile/continuations.h" #include "libguile/gc.h" #include "libguile/init.h" +#include "libguile/scmsigs.h" #ifdef __MINGW32__ #ifndef ETIMEDOUT @@ -131,6 +132,7 @@ { scm_i_thread *t = SCM_I_THREAD_DATA (obj); scm_gc_mark (t->result); + scm_gc_mark (t->cleanup_handler); scm_gc_mark (t->join_queue); scm_gc_mark (t->dynwinds); scm_gc_mark (t->active_asyncs); @@ -405,6 +407,8 @@ static SCM scm_i_default_dynamic_state; +extern scm_i_thread *scm_i_signal_delivery_thread; + /* Perform first stage of thread initialisation, in non-guile mode. */ static void @@ -415,6 +419,7 @@ t->pthread = scm_i_pthread_self (); t->handle = SCM_BOOL_F; t->result = SCM_BOOL_F; + t->cleanup_handler = SCM_BOOL_F; t->join_queue = SCM_EOL; t->dynamic_state = SCM_BOOL_F; t->dynwinds = SCM_EOL; @@ -434,6 +439,7 @@ scm_i_pthread_mutex_init (&t->heap_mutex, NULL); t->clear_freelists_p = 0; t->gc_running_p = 0; + t->canceled = 0; t->exited = 0; t->freelist = SCM_EOL; @@ -478,7 +484,16 @@ static void * do_thread_exit (void *v) { - scm_i_thread *t = (scm_i_thread *)v; + scm_i_thread *t = (scm_i_thread *) v; + + if (!scm_is_false (t->cleanup_handler)) + { + SCM ptr = t->cleanup_handler; + t->cleanup_handler = SCM_BOOL_F; + t->result = scm_internal_catch (SCM_BOOL_T, + (scm_t_catch_body) scm_call_0, ptr, + scm_handle_by_message_noexit, NULL); + } scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); @@ -489,6 +504,7 @@ ; scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return NULL; } @@ -517,6 +533,17 @@ break; } thread_count--; + + /* If there's only one other thread, it could be the signal delivery thread, + so we need to notify it to shut down by closing its read pipe. If it's not + the signal delivery thread, then closing the read pipe isn't going to + hurt. + */ + if (thread_count <= 1) + { + scm_i_close_signal_pipe (); + } + scm_i_pthread_mutex_unlock (&thread_admin_mutex); scm_i_pthread_setspecific (scm_i_thread_key, NULL); @@ -882,6 +909,74 @@ } #undef FUNC_NAME +SCM_DEFINE (scm_cancel_thread, "cancel-thread", 1, 0, 0, + (SCM thread), +"Asynchronously force the target @var{thread} to terminate. @var{thread} " +"cannot be the current thread, and if @var{thread} has already terminated or " +"been signaled to terminate, this function is a no-op.") +#define FUNC_NAME s_scm_cancel_thread +{ + scm_i_thread *t = NULL; + + SCM_VALIDATE_THREAD (1, thread); + t = SCM_I_THREAD_DATA (thread); + scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); + if (!t->canceled) + { + t->canceled = 1; + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + scm_i_pthread_cancel (t->pthread); + } + else + { + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + } + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_set_thread_cleanup_x, "set-thread-cleanup!", 2, 0, 0, + (SCM thread, SCM proc), +"Set the thunk @var{proc} as the cleanup handler for the thread @var{thread}. " +"This handler will be called when the thread exits.") +#define FUNC_NAME s_scm_set_thread_cleanup_x +{ + scm_i_thread *t; + + SCM_VALIDATE_THREAD (1, thread); + if (scm_is_true (proc) && scm_is_false (scm_thunk_p (proc))) + { + SCM_MISC_ERROR ("proc must be a thunk", SCM_EOL); + } + + scm_i_pthread_mutex_lock (&thread_admin_mutex); + t = SCM_I_THREAD_DATA (thread); + if (!(t->exited || t->canceled)) + { + t->cleanup_handler = proc; + } + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_current_thread_cleanup, "thread-cleanup", 1, 0, 0, + (SCM thread), +"Return the cleanup handler installed for the thread @var{thread}.") +#define FUNC_NAME s_scm_current_thread_cleanup +{ + scm_i_thread *t; + SCM ret; + + SCM_VALIDATE_THREAD (1, thread); + scm_i_pthread_mutex_lock (&thread_admin_mutex); + t = SCM_I_THREAD_DATA (thread); + ret = (t->exited || t->canceled) ? SCM_BOOL_F : t->cleanup_handler; + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return ret; +} +#undef FUNC_NAME + SCM_DEFINE (scm_join_thread, "join-thread", 1, 0, 0, (SCM thread), "Suspend execution of the calling thread until the target @var{thread} " @@ -1539,8 +1634,11 @@ l = &list; for (t = all_threads; t && n > 0; t = t->next_thread) { - SCM_SETCAR (*l, t->handle); - l = SCM_CDRLOC (*l); + if (t != scm_i_signal_delivery_thread) + { + SCM_SETCAR (*l, t->handle); + l = SCM_CDRLOC (*l); + } n--; } *l = SCM_EOL; @@ -1694,6 +1792,7 @@ scm_set_smob_free (scm_tc16_condvar, fat_cond_free); scm_i_default_dynamic_state = SCM_BOOL_F; + scm_i_pthread_setspecific (scm_i_thread_key, SCM_I_CURRENT_THREAD); guilify_self_2 (SCM_BOOL_F); threads_initialized_p = 1; Index: libguile/threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/threads.h,v retrieving revision 1.48 diff -a -u -r1.48 threads.h --- libguile/threads.h 17 Apr 2006 00:05:42 -0000 1.48 +++ libguile/threads.h 27 Sep 2007 02:20:34 -0000 @@ -49,9 +49,11 @@ SCM handle; scm_i_pthread_t pthread; - + + SCM cleanup_handler; SCM join_queue; SCM result; + int canceled; int exited; SCM sleep_object; @@ -153,6 +155,9 @@ SCM_API SCM scm_call_with_new_thread (SCM thunk, SCM handler); SCM_API SCM scm_yield (void); +SCM_API SCM scm_cancel_thread (SCM t); +SCM_API SCM scm_set_thread_cleanup_x (SCM thread, SCM proc); +SCM_API SCM scm_current_thread_cleanup (SCM thread); SCM_API SCM scm_join_thread (SCM t); SCM_API SCM scm_make_mutex (void); Index: test-suite/tests/threads.test =================================================================== RCS file: /sources/guile/guile/guile-core/test-suite/tests/threads.test,v retrieving revision 1.6 diff -a -u -r1.6 threads.test --- test-suite/tests/threads.test 17 Jun 2006 23:08:23 -0000 1.6 +++ test-suite/tests/threads.test 27 Sep 2007 02:20:35 -0000 @@ -133,4 +133,54 @@ (lambda (n) (set! result (cons n result))) (lambda (n) (* 2 n)) '(0 1 2 3 4 5)) - (equal? result '(10 8 6 4 2 0))))))) + (equal? result '(10 8 6 4 2 0))))) + + ;; + ;; thread cancellation + ;; + + (with-test-prefix "cancel-thread" + + (pass-if "cancel succeeds" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (begin (lock-mutex m) 'foo)))) + (cancel-thread t) + (join-thread t) + #t))) + + (pass-if "handler result passed to join" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (lock-mutex m)))) + (set-thread-cleanup! t (lambda () 'foo)) + (cancel-thread t) + (eq? (join-thread t) 'foo)))) + + (pass-if "can cancel self" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (begin + (set-thread-cleanup! (current-thread) + (lambda () 'foo)) + (cancel-thread (current-thread)) + (lock-mutex m))))) + (eq? (join-thread t) 'foo)))) + + (pass-if "handler supplants final expr" + (let ((t (begin-thread (begin (set-thread-cleanup! (current-thread) + (lambda () 'bar)) + 'foo)))) + (eq? (join-thread t) 'bar))) + + (pass-if "remove handler by setting false" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (lock-mutex m) 'bar))) + (set-thread-cleanup! t (lambda () 'foo)) + (set-thread-cleanup! t #f) + (unlock-mutex m) + (eq? (join-thread t) 'bar)))) + + (pass-if "initial handler is false" + (not (thread-cleanup (current-thread))))))) [-- Attachment #3: Type: text/plain, Size: 143 bytes --] _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-09-27 2:39 ` Julian Graham @ 2007-10-18 0:41 ` Julian Graham 2007-10-20 11:12 ` Ludovic Courtès 2007-10-20 13:02 ` Ludovic Courtès 0 siblings, 2 replies; 23+ messages in thread From: Julian Graham @ 2007-10-18 0:41 UTC (permalink / raw) To: Ludovic Courtès; +Cc: guile-devel [-- Attachment #1: Type: text/plain, Size: 1214 bytes --] While testing some of the changes I made for SRFI-18 support, I noticed a couple of deadlocks in my thread cancellation code. I've updated my thread cancellation patch to address them. Specifically: * If a thread was interrupted by a cancellation while it was in Guile mode (i.e., holding its heap mutex), a GC in another thread could cause a deadlock by grabbing the thread admin mutex and then attempting to seize the canceled thread's heap mutex -- the canceled thread only releases its heap mutex once during cancellation (the heap mutexes are recursive). I've added a pthread cleanup handler to deal with this case by releasing a thread's heap mutex before beginning the exit procedure. * If the signal delivery thread got launched a little bit too late, it could be holding its startup mutex and then attempt to grab the thread_admin_mutex, which could be held by a thread that was in the process of being canceled and which was trying to obtain the signal delivery thread's startup mutex. I've resolved this by forcing the signal delivery thread to start (if it hasn't already) during thread cancellation of any thread. Find attached a new version of the thread cancellation patch. Regards, Julian [-- Attachment #2: thread-cancellation.HEAD.patch-20071017 --] [-- Type: application/octet-stream, Size: 14928 bytes --] Index: libguile/null-threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/null-threads.h,v retrieving revision 1.12 diff -a -u -r1.12 null-threads.h --- libguile/null-threads.h 17 Apr 2006 00:05:40 -0000 1.12 +++ libguile/null-threads.h 18 Oct 2007 00:11:00 -0000 @@ -41,6 +41,9 @@ #define scm_i_pthread_create(t,a,f,d) (*(t)=0, (void)(f), ENOSYS) #define scm_i_pthread_detach(t) do { } while (0) #define scm_i_pthread_exit(v) exit(0) +#define scm_i_pthread_cancel(t) 0 +#define scm_i_pthread_cleanup_push(t,v) 0 +#define scm_i_pthread_cleanup_pop(e) 0 #define scm_i_sched_yield() 0 /* Signals Index: libguile/pthread-threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/pthread-threads.h,v retrieving revision 1.16 diff -a -u -r1.16 pthread-threads.h --- libguile/pthread-threads.h 10 Oct 2007 16:46:26 -0000 1.16 +++ libguile/pthread-threads.h 18 Oct 2007 00:11:00 -0000 @@ -35,6 +35,9 @@ #define scm_i_pthread_create pthread_create #define scm_i_pthread_detach pthread_detach #define scm_i_pthread_exit pthread_exit +#define scm_i_pthread_cancel pthread_cancel +#define scm_i_pthread_cleanup_push pthread_cleanup_push +#define scm_i_pthread_cleanup_pop pthread_cleanup_pop #define scm_i_sched_yield sched_yield /* Signals Index: libguile/scmsigs.c =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/scmsigs.c,v retrieving revision 1.97 diff -a -u -r1.97 scmsigs.c --- libguile/scmsigs.c 7 Mar 2007 23:10:52 -0000 1.97 +++ libguile/scmsigs.c 18 Oct 2007 00:11:01 -0000 @@ -33,6 +33,7 @@ #include "libguile/eval.h" #include "libguile/root.h" #include "libguile/vectors.h" +#include "libguile/threads.h" #include "libguile/validate.h" #include "libguile/scmsigs.h" @@ -99,6 +100,9 @@ static SCM signal_handler_asyncs; static SCM signal_handler_threads; +scm_i_thread *scm_i_signal_delivery_thread; +static scm_i_pthread_mutex_t signal_delivery_thread_mutex; + /* saves the original C handlers, when a new handler is installed. set to SIG_ERR if the original handler is installed. */ #ifdef HAVE_SIGACTION @@ -185,24 +189,29 @@ if (scm_is_true (h)) scm_system_async_mark_for_thread (h, t); } + else if (n == 0) + break; /* the signal pipe was closed. */ else if (n < 0 && errno != EINTR) perror ("error in signal delivery thread"); } - return SCM_UNSPECIFIED; /* not reached */ + return SCM_UNSPECIFIED; /* not reached unless all other threads exited */ } static void start_signal_delivery_thread (void) { + scm_i_pthread_mutex_lock (&signal_delivery_thread_mutex); if (pipe (signal_pipe) != 0) scm_syserror (NULL); - scm_spawn_thread (signal_delivery_thread, NULL, - scm_handle_by_message, "signal delivery thread"); + scm_i_signal_delivery_thread = SCM_I_THREAD_DATA + (scm_spawn_thread (signal_delivery_thread, NULL, + scm_handle_by_message, "signal delivery thread")); + scm_i_pthread_mutex_unlock (&signal_delivery_thread_mutex); } -static void -ensure_signal_delivery_thread () +void +scm_i_ensure_signal_delivery_thread () { static scm_i_pthread_once_t once = SCM_I_PTHREAD_ONCE_INIT; scm_i_pthread_once (&once, start_signal_delivery_thread); @@ -228,8 +237,8 @@ #endif } -static void -ensure_signal_delivery_thread () +void +scm_i_ensure_signal_delivery_thread () { return; } @@ -332,7 +341,7 @@ SCM_MISC_ERROR ("thread has already exited", SCM_EOL); } - ensure_signal_delivery_thread (); + scm_i_ensure_signal_delivery_thread (); SCM_CRITICAL_SECTION_START; old_handler = SCM_SIMPLE_VECTOR_REF (*signal_handlers, csig); @@ -653,10 +662,29 @@ \f void +scm_i_close_signal_pipe() +{ + /* signal_delivery_thread_mutex will only be locked while the signal + delivery thread is being launched. The thread that calls this function + is already holding the thread admin mutex, so if the delivery thread + hasn't been launched at this point, it never will be before shutdown. + */ + scm_i_pthread_mutex_lock (&signal_delivery_thread_mutex); + if (scm_i_signal_delivery_thread != NULL) + { + close (signal_pipe[1]); + } + scm_i_pthread_mutex_unlock (&signal_delivery_thread_mutex); +} + +void scm_init_scmsigs () { int i; + scm_i_pthread_mutex_init (&signal_delivery_thread_mutex, NULL); + scm_i_signal_delivery_thread = NULL; + signal_handlers = SCM_VARIABLE_LOC (scm_c_define ("signal-handlers", scm_c_make_vector (NSIG, SCM_BOOL_F))); Index: libguile/scmsigs.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/scmsigs.h,v retrieving revision 1.17 diff -a -u -r1.17 scmsigs.h --- libguile/scmsigs.h 17 Apr 2006 00:05:40 -0000 1.17 +++ libguile/scmsigs.h 18 Oct 2007 00:11:01 -0000 @@ -41,6 +41,9 @@ SCM_API SCM scm_raise (SCM sig); SCM_API void scm_init_scmsigs (void); +SCM_API void scm_i_close_signal_pipe (void); +SCM_API void scm_i_ensure_signal_delivery_thread (void); + #endif /* SCM_SCMSIGS_H */ /* Index: libguile/threads.c =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/threads.c,v retrieving revision 1.89 diff -a -u -r1.89 threads.c --- libguile/threads.c 2 Oct 2007 16:06:25 -0000 1.89 +++ libguile/threads.c 18 Oct 2007 00:11:03 -0000 @@ -48,6 +48,7 @@ #include "libguile/continuations.h" #include "libguile/gc.h" #include "libguile/init.h" +#include "libguile/scmsigs.h" #ifdef __MINGW32__ #ifndef ETIMEDOUT @@ -131,6 +132,7 @@ { scm_i_thread *t = SCM_I_THREAD_DATA (obj); scm_gc_mark (t->result); + scm_gc_mark (t->cleanup_handler); scm_gc_mark (t->join_queue); scm_gc_mark (t->dynwinds); scm_gc_mark (t->active_asyncs); @@ -405,6 +407,8 @@ static SCM scm_i_default_dynamic_state; +extern scm_i_thread *scm_i_signal_delivery_thread; + /* Perform first stage of thread initialisation, in non-guile mode. */ static void @@ -415,6 +419,7 @@ t->pthread = scm_i_pthread_self (); t->handle = SCM_BOOL_F; t->result = SCM_BOOL_F; + t->cleanup_handler = SCM_BOOL_F; t->join_queue = SCM_EOL; t->dynamic_state = SCM_BOOL_F; t->dynwinds = SCM_EOL; @@ -434,6 +439,7 @@ scm_i_pthread_mutex_init (&t->heap_mutex, NULL); t->clear_freelists_p = 0; t->gc_running_p = 0; + t->canceled = 0; t->exited = 0; t->freelist = SCM_EOL; @@ -478,7 +484,16 @@ static void * do_thread_exit (void *v) { - scm_i_thread *t = (scm_i_thread *)v; + scm_i_thread *t = (scm_i_thread *) v; + + if (!scm_is_false (t->cleanup_handler)) + { + SCM ptr = t->cleanup_handler; + t->cleanup_handler = SCM_BOOL_F; + t->result = scm_internal_catch (SCM_BOOL_T, + (scm_t_catch_body) scm_call_0, ptr, + scm_handle_by_message_noexit, NULL); + } scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); @@ -489,6 +504,7 @@ ; scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return NULL; } @@ -496,10 +512,15 @@ on_thread_exit (void *v) { /* This handler is executed in non-guile mode. */ - scm_i_thread *t = (scm_i_thread *)v, **tp; + scm_i_thread *t = (scm_i_thread *) v, **tp; scm_i_pthread_setspecific (scm_i_thread_key, v); + /* Ensure the signal handling thread has been launched, because we might be + shutting it down. */ + + scm_i_ensure_signal_delivery_thread (); + /* Unblocking the joining threads needs to happen in guile mode since the queue is a SCM data structure. */ scm_with_guile (do_thread_exit, v); @@ -515,6 +536,17 @@ break; } thread_count--; + + /* If there's only one other thread, it could be the signal delivery thread, + so we need to notify it to shut down by closing its read pipe. If it's not + the signal delivery thread, then closing the read pipe isn't going to + hurt. + */ + if (thread_count <= 1) + { + scm_i_close_signal_pipe (); + } + scm_i_pthread_mutex_unlock (&thread_admin_mutex); scm_i_pthread_setspecific (scm_i_thread_key, NULL); @@ -684,17 +716,30 @@ scm_i_default_dynamic_state); } +static void +scm_leave_guile_cleanup (void *x) +{ + scm_leave_guile (); +} + void * -scm_i_with_guile_and_parent (void *(*func)(void *), void *data, - SCM parent) +scm_i_with_guile_and_parent (void *(*func)(void *), void *data, SCM parent) { void *res; int really_entered; SCM_STACKITEM base_item; + really_entered = scm_i_init_thread_for_guile (&base_item, parent); - res = scm_c_with_continuation_barrier (func, data); if (really_entered) - scm_leave_guile (); + { + scm_i_pthread_cleanup_push (scm_leave_guile_cleanup, NULL); + res = scm_c_with_continuation_barrier (func, data); + scm_i_pthread_cleanup_pop (0); + scm_leave_guile (); + } + else + res = scm_c_with_continuation_barrier (func, data); + return res; } @@ -880,6 +925,74 @@ } #undef FUNC_NAME +SCM_DEFINE (scm_cancel_thread, "cancel-thread", 1, 0, 0, + (SCM thread), +"Asynchronously force the target @var{thread} to terminate. @var{thread} " +"cannot be the current thread, and if @var{thread} has already terminated or " +"been signaled to terminate, this function is a no-op.") +#define FUNC_NAME s_scm_cancel_thread +{ + scm_i_thread *t = NULL; + + SCM_VALIDATE_THREAD (1, thread); + t = SCM_I_THREAD_DATA (thread); + scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); + if (!t->canceled) + { + t->canceled = 1; + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + scm_i_pthread_cancel (t->pthread); + } + else + { + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + } + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_set_thread_cleanup_x, "set-thread-cleanup!", 2, 0, 0, + (SCM thread, SCM proc), +"Set the thunk @var{proc} as the cleanup handler for the thread @var{thread}. " +"This handler will be called when the thread exits.") +#define FUNC_NAME s_scm_set_thread_cleanup_x +{ + scm_i_thread *t; + + SCM_VALIDATE_THREAD (1, thread); + if (scm_is_true (proc) && scm_is_false (scm_thunk_p (proc))) + { + SCM_MISC_ERROR ("proc must be a thunk", SCM_EOL); + } + + scm_i_pthread_mutex_lock (&thread_admin_mutex); + t = SCM_I_THREAD_DATA (thread); + if (!(t->exited || t->canceled)) + { + t->cleanup_handler = proc; + } + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_current_thread_cleanup, "thread-cleanup", 1, 0, 0, + (SCM thread), +"Return the cleanup handler installed for the thread @var{thread}.") +#define FUNC_NAME s_scm_current_thread_cleanup +{ + scm_i_thread *t; + SCM ret; + + SCM_VALIDATE_THREAD (1, thread); + scm_i_pthread_mutex_lock (&thread_admin_mutex); + t = SCM_I_THREAD_DATA (thread); + ret = (t->exited || t->canceled) ? SCM_BOOL_F : t->cleanup_handler; + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return ret; +} +#undef FUNC_NAME + SCM_DEFINE (scm_join_thread, "join-thread", 1, 0, 0, (SCM thread), "Suspend execution of the calling thread until the target @var{thread} " @@ -1537,8 +1650,11 @@ l = &list; for (t = all_threads; t && n > 0; t = t->next_thread) { - SCM_SETCAR (*l, t->handle); - l = SCM_CDRLOC (*l); + if (t != scm_i_signal_delivery_thread) + { + SCM_SETCAR (*l, t->handle); + l = SCM_CDRLOC (*l); + } n--; } *l = SCM_EOL; @@ -1692,6 +1808,7 @@ scm_set_smob_free (scm_tc16_condvar, fat_cond_free); scm_i_default_dynamic_state = SCM_BOOL_F; + scm_i_pthread_setspecific (scm_i_thread_key, SCM_I_CURRENT_THREAD); guilify_self_2 (SCM_BOOL_F); threads_initialized_p = 1; Index: libguile/threads.h =================================================================== RCS file: /sources/guile/guile/guile-core/libguile/threads.h,v retrieving revision 1.48 diff -a -u -r1.48 threads.h --- libguile/threads.h 17 Apr 2006 00:05:42 -0000 1.48 +++ libguile/threads.h 18 Oct 2007 00:11:03 -0000 @@ -49,9 +49,11 @@ SCM handle; scm_i_pthread_t pthread; - + + SCM cleanup_handler; SCM join_queue; SCM result; + int canceled; int exited; SCM sleep_object; @@ -153,6 +155,9 @@ SCM_API SCM scm_call_with_new_thread (SCM thunk, SCM handler); SCM_API SCM scm_yield (void); +SCM_API SCM scm_cancel_thread (SCM t); +SCM_API SCM scm_set_thread_cleanup_x (SCM thread, SCM proc); +SCM_API SCM scm_current_thread_cleanup (SCM thread); SCM_API SCM scm_join_thread (SCM t); SCM_API SCM scm_make_mutex (void); Index: test-suite/tests/threads.test =================================================================== RCS file: /sources/guile/guile/guile-core/test-suite/tests/threads.test,v retrieving revision 1.6 diff -a -u -r1.6 threads.test --- test-suite/tests/threads.test 17 Jun 2006 23:08:23 -0000 1.6 +++ test-suite/tests/threads.test 18 Oct 2007 00:11:04 -0000 @@ -133,4 +133,54 @@ (lambda (n) (set! result (cons n result))) (lambda (n) (* 2 n)) '(0 1 2 3 4 5)) - (equal? result '(10 8 6 4 2 0))))))) + (equal? result '(10 8 6 4 2 0))))) + + ;; + ;; thread cancellation + ;; + + (with-test-prefix "cancel-thread" + + (pass-if "cancel succeeds" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (begin (lock-mutex m) 'foo)))) + (cancel-thread t) + (join-thread t) + #t))) + + (pass-if "handler result passed to join" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (lock-mutex m)))) + (set-thread-cleanup! t (lambda () 'foo)) + (cancel-thread t) + (eq? (join-thread t) 'foo)))) + + (pass-if "can cancel self" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (begin + (set-thread-cleanup! (current-thread) + (lambda () 'foo)) + (cancel-thread (current-thread)) + (lock-mutex m))))) + (eq? (join-thread t) 'foo)))) + + (pass-if "handler supplants final expr" + (let ((t (begin-thread (begin (set-thread-cleanup! (current-thread) + (lambda () 'bar)) + 'foo)))) + (eq? (join-thread t) 'bar))) + + (pass-if "remove handler by setting false" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (lock-mutex m) 'bar))) + (set-thread-cleanup! t (lambda () 'foo)) + (set-thread-cleanup! t #f) + (unlock-mutex m) + (eq? (join-thread t) 'bar)))) + + (pass-if "initial handler is false" + (not (thread-cleanup (current-thread))))))) [-- Attachment #3: Type: text/plain, Size: 143 bytes --] _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-18 0:41 ` Julian Graham @ 2007-10-20 11:12 ` Ludovic Courtès 2007-10-20 13:02 ` Ludovic Courtès 1 sibling, 0 replies; 23+ messages in thread From: Ludovic Courtès @ 2007-10-20 11:12 UTC (permalink / raw) To: guile-devel [-- Attachment #1: Type: text/plain, Size: 447 bytes --] Hi Julian, I went through your patch again and installed a slightly modified version, mostly formatting issues (attached). I guess you're now more familiar than I am with specific pthread issues, so I could have missed a few points. You changed `all-threads' so that it hides the signal-delivery thread. It seems harmless and reasonable, so I installed it too. If others can think of reasons not to do it, please speak up. Thanks! Ludovic. [-- Attachment #2: The patch --] [-- Type: text/x-patch, Size: 18495 bytes --] --- orig/ChangeLog +++ mod/ChangeLog @@ -1,3 +1,7 @@ +2007-10-20 Julian Graham <joolean@gmail.com> + + * NEWS: Mention thread cancellation and cleanup API. + 2007-10-17 Ludovic Courtès <ludo@gnu.org> * NEWS: Mention reader bug-fix. --- orig/NEWS +++ mod/NEWS @@ -26,6 +26,9 @@ be used for efficiently implementing a S ** Duplicate bindings among used modules are resolved lazily. This slightly improves program startup times. +** New thread cancellation and thread cleanup API +See `cancel-thread', `set-thread-cleanup!', and `thread-cleanup'. + * Changes to the C interface ** Functions for handling `scm_option' now no longer require an argument --- orig/libguile/ChangeLog +++ mod/libguile/ChangeLog @@ -1,3 +1,42 @@ +2007-10-20 Julian Graham <joolean@gmail.com> + + Add support for thread cancellation and user-defined thread + cleanup handlers. Small rework by Ludovic Courtès. + + * null-threads.h (scm_i_pthread_cancel, + scm_i_pthread_cleanup_push, scm_i_pthread_cleanup_pop): New. + * pthread-threads.h (scm_i_pthread_cancel, + scm_i_pthread_cleanup_push, scm_i_pthread_cleanup_pop): New. + * scmsigs.c (scm_i_signal_delivery_thread, + signal_delivery_thread_mutex): New. + (signal_delivery_thread): Leave when `read_without_guile ()' + returns zero. + (start_signal_delivery_thread): Acquire SIGNAL_DELIVERY_THREAD + before spawning the thread. Initialize + SCM_I_SIGNAL_DELIVERY_THREAD. + (ensure_signal_delivery_thread): Renamed to... + (scm_i_ensure_signal_delivery_thread): this. + (scm_i_close_signal_pipe): New. + * scmsigs.h: Updated. + * threads.c (thread_mark): Mark `t->cleanup_handler'. + (guilify_self_1): Initialize `t->cleanup_handler' and + `t->canceled'. + (do_thread_exit): Invoke `t->cleanup_handler'. + (on_thread_exit): Call `scm_i_ensure_signal_delivery_thread ()'. + Call `scm_i_close_signal_pipe ()' when the next-to-last thread + vanishes. + (scm_leave_guile_cleanup): New. + (scm_i_with_guile_and_parent): Use `scm_i_pthread_cleanup_push ()' + and `scm_leave_guile_cleanup ()' to leave guile mode, rather + than call `scm_leave_guile ()' after FUNC. + (scm_cancel_thread, scm_set_thread_cleanup_x, + scm_threads_cleanup): New. + (scm_all_threads): Remove SCM_I_SIGNAL_DELIVERY_THREAD from the + returned list. + * threads.h (scm_i_thread)[cleanup_handler, canceled]: New + fields. + Add declarations of new functions. + 2007-10-17 Ludovic Courtès <ludo@gnu.org> * read.c (CHAR_IS_BLANK_): Add `\r' (ASCII 0x0d). This fixes a --- orig/libguile/null-threads.h +++ mod/libguile/null-threads.h @@ -41,6 +41,9 @@ #define scm_i_pthread_create(t,a,f,d) (*(t)=0, (void)(f), ENOSYS) #define scm_i_pthread_detach(t) do { } while (0) #define scm_i_pthread_exit(v) exit(0) +#define scm_i_pthread_cancel(t) 0 +#define scm_i_pthread_cleanup_push(t,v) 0 +#define scm_i_pthread_cleanup_pop(e) 0 #define scm_i_sched_yield() 0 /* Signals --- orig/libguile/pthread-threads.h +++ mod/libguile/pthread-threads.h @@ -35,6 +35,9 @@ #define scm_i_pthread_create pthread_create #define scm_i_pthread_detach pthread_detach #define scm_i_pthread_exit pthread_exit +#define scm_i_pthread_cancel pthread_cancel +#define scm_i_pthread_cleanup_push pthread_cleanup_push +#define scm_i_pthread_cleanup_pop pthread_cleanup_pop #define scm_i_sched_yield sched_yield /* Signals --- orig/libguile/scmsigs.c +++ mod/libguile/scmsigs.c @@ -33,6 +33,7 @@ #include "libguile/eval.h" #include "libguile/root.h" #include "libguile/vectors.h" +#include "libguile/threads.h" #include "libguile/validate.h" #include "libguile/scmsigs.h" @@ -99,6 +100,14 @@ static SCM *signal_handlers; static SCM signal_handler_asyncs; static SCM signal_handler_threads; +/* The signal delivery thread. */ +scm_i_thread *scm_i_signal_delivery_thread = NULL; + +/* The mutex held when launching the signal delivery thread. */ +static scm_i_pthread_mutex_t signal_delivery_thread_mutex = + SCM_I_PTHREAD_MUTEX_INITIALIZER; + + /* saves the original C handlers, when a new handler is installed. set to SIG_ERR if the original handler is installed. */ #ifdef HAVE_SIGACTION @@ -185,24 +194,34 @@ signal_delivery_thread (void *data) if (scm_is_true (h)) scm_system_async_mark_for_thread (h, t); } + else if (n == 0) + break; /* the signal pipe was closed. */ else if (n < 0 && errno != EINTR) perror ("error in signal delivery thread"); } - return SCM_UNSPECIFIED; /* not reached */ + return SCM_UNSPECIFIED; /* not reached unless all other threads exited */ } static void start_signal_delivery_thread (void) { + SCM signal_thread; + + scm_i_pthread_mutex_lock (&signal_delivery_thread_mutex); + if (pipe (signal_pipe) != 0) scm_syserror (NULL); - scm_spawn_thread (signal_delivery_thread, NULL, - scm_handle_by_message, "signal delivery thread"); + signal_thread = scm_spawn_thread (signal_delivery_thread, NULL, + scm_handle_by_message, + "signal delivery thread"); + scm_i_signal_delivery_thread = SCM_I_THREAD_DATA (signal_thread); + + scm_i_pthread_mutex_unlock (&signal_delivery_thread_mutex); } -static void -ensure_signal_delivery_thread () +void +scm_i_ensure_signal_delivery_thread () { static scm_i_pthread_once_t once = SCM_I_PTHREAD_ONCE_INIT; scm_i_pthread_once (&once, start_signal_delivery_thread); @@ -228,8 +247,8 @@ take_signal (int signum) #endif } -static void -ensure_signal_delivery_thread () +void +scm_i_ensure_signal_delivery_thread () { return; } @@ -332,7 +351,7 @@ SCM_DEFINE (scm_sigaction_for_thread, "s SCM_MISC_ERROR ("thread has already exited", SCM_EOL); } - ensure_signal_delivery_thread (); + scm_i_ensure_signal_delivery_thread (); SCM_CRITICAL_SECTION_START; old_handler = SCM_SIMPLE_VECTOR_REF (*signal_handlers, csig); @@ -653,6 +672,21 @@ SCM_DEFINE (scm_raise, "raise", 1, 0, 0, \f void +scm_i_close_signal_pipe() +{ + /* SIGNAL_DELIVERY_THREAD_MUTEX is only locked while the signal delivery + thread is being launched. The thread that calls this function is + already holding the thread admin mutex, so if the delivery thread hasn't + been launched at this point, it never will be before shutdown. */ + scm_i_pthread_mutex_lock (&signal_delivery_thread_mutex); + + if (scm_i_signal_delivery_thread != NULL) + close (signal_pipe[1]); + + scm_i_pthread_mutex_unlock (&signal_delivery_thread_mutex); +} + +void scm_init_scmsigs () { int i; --- orig/libguile/scmsigs.h +++ mod/libguile/scmsigs.h @@ -3,7 +3,7 @@ #ifndef SCM_SCMSIGS_H #define SCM_SCMSIGS_H -/* Copyright (C) 1995,1996,1997,1998,2000, 2002, 2006 Free Software Foundation, Inc. +/* Copyright (C) 1995,1996,1997,1998,2000, 2002, 2006, 2007 Free Software Foundation, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -23,6 +23,7 @@ \f #include "libguile/__scm.h" +#include "libguile/threads.h" \f @@ -41,6 +42,11 @@ SCM_API SCM scm_usleep (SCM i); SCM_API SCM scm_raise (SCM sig); SCM_API void scm_init_scmsigs (void); +SCM_API void scm_i_close_signal_pipe (void); +SCM_API void scm_i_ensure_signal_delivery_thread (void); + +SCM_API scm_i_thread *scm_i_signal_delivery_thread; + #endif /* SCM_SCMSIGS_H */ /* --- orig/libguile/threads.c +++ mod/libguile/threads.c @@ -1,4 +1,4 @@ -/* Copyright (C) 1995,1996,1997,1998,2000,2001, 2002, 2003, 2004, 2005, 2006 Free Software Foundation, Inc. +/* Copyright (C) 1995,1996,1997,1998,2000,2001, 2002, 2003, 2004, 2005, 2006, 2007 Free Software Foundation, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -48,6 +48,7 @@ #include "libguile/continuations.h" #include "libguile/gc.h" #include "libguile/init.h" +#include "libguile/scmsigs.h" #ifdef __MINGW32__ #ifndef ETIMEDOUT @@ -131,6 +132,7 @@ thread_mark (SCM obj) { scm_i_thread *t = SCM_I_THREAD_DATA (obj); scm_gc_mark (t->result); + scm_gc_mark (t->cleanup_handler); scm_gc_mark (t->join_queue); scm_gc_mark (t->dynwinds); scm_gc_mark (t->active_asyncs); @@ -415,6 +417,7 @@ guilify_self_1 (SCM_STACKITEM *base) t->pthread = scm_i_pthread_self (); t->handle = SCM_BOOL_F; t->result = SCM_BOOL_F; + t->cleanup_handler = SCM_BOOL_F; t->join_queue = SCM_EOL; t->dynamic_state = SCM_BOOL_F; t->dynwinds = SCM_EOL; @@ -434,6 +437,7 @@ guilify_self_1 (SCM_STACKITEM *base) scm_i_pthread_mutex_init (&t->heap_mutex, NULL); t->clear_freelists_p = 0; t->gc_running_p = 0; + t->canceled = 0; t->exited = 0; t->freelist = SCM_EOL; @@ -478,7 +482,17 @@ guilify_self_2 (SCM parent) static void * do_thread_exit (void *v) { - scm_i_thread *t = (scm_i_thread *)v; + scm_i_thread *t = (scm_i_thread *) v; + + if (!scm_is_false (t->cleanup_handler)) + { + SCM ptr = t->cleanup_handler; + + t->cleanup_handler = SCM_BOOL_F; + t->result = scm_internal_catch (SCM_BOOL_T, + (scm_t_catch_body) scm_call_0, ptr, + scm_handle_by_message_noexit, NULL); + } scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); @@ -489,6 +503,7 @@ do_thread_exit (void *v) ; scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return NULL; } @@ -496,10 +511,14 @@ static void on_thread_exit (void *v) { /* This handler is executed in non-guile mode. */ - scm_i_thread *t = (scm_i_thread *)v, **tp; + scm_i_thread *t = (scm_i_thread *) v, **tp; scm_i_pthread_setspecific (scm_i_thread_key, v); + /* Ensure the signal handling thread has been launched, because we might be + shutting it down. */ + scm_i_ensure_signal_delivery_thread (); + /* Unblocking the joining threads needs to happen in guile mode since the queue is a SCM data structure. */ scm_with_guile (do_thread_exit, v); @@ -515,6 +534,14 @@ on_thread_exit (void *v) break; } thread_count--; + + /* If there's only one other thread, it could be the signal delivery + thread, so we need to notify it to shut down by closing its read pipe. + If it's not the signal delivery thread, then closing the read pipe isn't + going to hurt. */ + if (thread_count <= 1) + scm_i_close_signal_pipe (); + scm_i_pthread_mutex_unlock (&thread_admin_mutex); scm_i_pthread_setspecific (scm_i_thread_key, NULL); @@ -684,17 +711,30 @@ scm_with_guile (void *(*func)(void *), v scm_i_default_dynamic_state); } +static void +scm_leave_guile_cleanup (void *x) +{ + scm_leave_guile (); +} + void * -scm_i_with_guile_and_parent (void *(*func)(void *), void *data, - SCM parent) +scm_i_with_guile_and_parent (void *(*func)(void *), void *data, SCM parent) { void *res; int really_entered; SCM_STACKITEM base_item; + really_entered = scm_i_init_thread_for_guile (&base_item, parent); - res = scm_c_with_continuation_barrier (func, data); if (really_entered) - scm_leave_guile (); + { + scm_i_pthread_cleanup_push (scm_leave_guile_cleanup, NULL); + res = scm_c_with_continuation_barrier (func, data); + scm_i_pthread_cleanup_pop (0); + scm_leave_guile (); + } + else + res = scm_c_with_continuation_barrier (func, data); + return res; } @@ -880,6 +920,74 @@ SCM_DEFINE (scm_yield, "yield", 0, 0, 0, } #undef FUNC_NAME +SCM_DEFINE (scm_cancel_thread, "cancel-thread", 1, 0, 0, + (SCM thread), +"Asynchronously force the target @var{thread} to terminate. @var{thread} " +"cannot be the current thread, and if @var{thread} has already terminated or " +"been signaled to terminate, this function is a no-op.") +#define FUNC_NAME s_scm_cancel_thread +{ + scm_i_thread *t = NULL; + + SCM_VALIDATE_THREAD (1, thread); + t = SCM_I_THREAD_DATA (thread); + scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); + if (!t->canceled) + { + t->canceled = 1; + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + scm_i_pthread_cancel (t->pthread); + } + else + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_set_thread_cleanup_x, "set-thread-cleanup!", 2, 0, 0, + (SCM thread, SCM proc), +"Set the thunk @var{proc} as the cleanup handler for the thread @var{thread}. " +"This handler will be called when the thread exits.") +#define FUNC_NAME s_scm_set_thread_cleanup_x +{ + scm_i_thread *t; + + SCM_VALIDATE_THREAD (1, thread); + if (!scm_is_false (proc)) + SCM_VALIDATE_THUNK (2, proc); + + scm_i_pthread_mutex_lock (&thread_admin_mutex); + + t = SCM_I_THREAD_DATA (thread); + if (!(t->exited || t->canceled)) + t->cleanup_handler = proc; + + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE (scm_thread_cleanup, "thread-cleanup", 1, 0, 0, + (SCM thread), +"Return the cleanup handler installed for the thread @var{thread}.") +#define FUNC_NAME s_scm_thread_cleanup +{ + scm_i_thread *t; + SCM ret; + + SCM_VALIDATE_THREAD (1, thread); + + scm_i_pthread_mutex_lock (&thread_admin_mutex); + t = SCM_I_THREAD_DATA (thread); + ret = (t->exited || t->canceled) ? SCM_BOOL_F : t->cleanup_handler; + scm_i_pthread_mutex_unlock (&thread_admin_mutex); + + return ret; +} +#undef FUNC_NAME + SCM_DEFINE (scm_join_thread, "join-thread", 1, 0, 0, (SCM thread), "Suspend execution of the calling thread until the target @var{thread} " @@ -891,7 +999,7 @@ SCM_DEFINE (scm_join_thread, "join-threa SCM_VALIDATE_THREAD (1, thread); if (scm_is_eq (scm_current_thread (), thread)) - SCM_MISC_ERROR ("can not join the current thread", SCM_EOL); + SCM_MISC_ERROR ("cannot join the current thread", SCM_EOL); scm_i_scm_pthread_mutex_lock (&thread_admin_mutex); @@ -911,10 +1019,13 @@ SCM_DEFINE (scm_join_thread, "join-threa res = t->result; scm_i_pthread_mutex_unlock (&thread_admin_mutex); + return res; } #undef FUNC_NAME + +\f /*** Fat mutexes */ /* We implement our own mutex type since we want them to be 'fair', we @@ -1537,8 +1648,11 @@ SCM_DEFINE (scm_all_threads, "all-thread l = &list; for (t = all_threads; t && n > 0; t = t->next_thread) { - SCM_SETCAR (*l, t->handle); - l = SCM_CDRLOC (*l); + if (t != scm_i_signal_delivery_thread) + { + SCM_SETCAR (*l, t->handle); + l = SCM_CDRLOC (*l); + } n--; } *l = SCM_EOL; --- orig/libguile/threads.h +++ mod/libguile/threads.h @@ -3,7 +3,7 @@ #ifndef SCM_THREADS_H #define SCM_THREADS_H -/* Copyright (C) 1996,1997,1998,2000,2001, 2002, 2003, 2004, 2006 Free Software Foundation, Inc. +/* Copyright (C) 1996,1997,1998,2000,2001, 2002, 2003, 2004, 2006, 2007 Free Software Foundation, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -49,9 +49,11 @@ typedef struct scm_i_thread { SCM handle; scm_i_pthread_t pthread; - + + SCM cleanup_handler; SCM join_queue; SCM result; + int canceled; int exited; SCM sleep_object; @@ -153,6 +155,9 @@ do { \ SCM_API SCM scm_call_with_new_thread (SCM thunk, SCM handler); SCM_API SCM scm_yield (void); +SCM_API SCM scm_cancel_thread (SCM t); +SCM_API SCM scm_set_thread_cleanup_x (SCM thread, SCM proc); +SCM_API SCM scm_thread_cleanup (SCM thread); SCM_API SCM scm_join_thread (SCM t); SCM_API SCM scm_make_mutex (void); --- orig/test-suite/ChangeLog +++ mod/test-suite/ChangeLog @@ -1,3 +1,10 @@ +2007-10-20 Julian Graham <joolean@gmail.com> + + * tests/threads.test: Use proper `define-module'. + (cancel-thread, handler result passed to join, can cancel self, + handler supplants final expr, remove handler by setting false, + initial handler is false): New tests. + 2007-10-17 Ludovic Courtès <ludo@gnu.org> * tests/reader.test (reading)[CR recognized as a token --- orig/test-suite/tests/threads.test +++ mod/test-suite/tests/threads.test @@ -1,6 +1,6 @@ ;;;; threads.test --- Tests for Guile threading. -*- scheme -*- ;;;; -;;;; Copyright 2003, 2006 Free Software Foundation, Inc. +;;;; Copyright 2003, 2006, 2007 Free Software Foundation, Inc. ;;;; ;;;; This program is free software; you can redistribute it and/or modify ;;;; it under the terms of the GNU General Public License as published by @@ -17,8 +17,10 @@ ;;;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, ;;;; Boston, MA 02110-1301 USA -(use-modules (ice-9 threads) - (test-suite lib)) +(define-module (test-threads) + :use-module (ice-9 threads) + :use-module (test-suite lib)) + (if (provided? 'threads) (begin @@ -133,4 +135,54 @@ (lambda (n) (set! result (cons n result))) (lambda (n) (* 2 n)) '(0 1 2 3 4 5)) - (equal? result '(10 8 6 4 2 0))))))) + (equal? result '(10 8 6 4 2 0))))) + + ;; + ;; thread cancellation + ;; + + (with-test-prefix "cancel-thread" + + (pass-if "cancel succeeds" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (begin (lock-mutex m) 'foo)))) + (cancel-thread t) + (join-thread t) + #t))) + + (pass-if "handler result passed to join" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (lock-mutex m)))) + (set-thread-cleanup! t (lambda () 'foo)) + (cancel-thread t) + (eq? (join-thread t) 'foo)))) + + (pass-if "can cancel self" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (begin + (set-thread-cleanup! (current-thread) + (lambda () 'foo)) + (cancel-thread (current-thread)) + (lock-mutex m))))) + (eq? (join-thread t) 'foo)))) + + (pass-if "handler supplants final expr" + (let ((t (begin-thread (begin (set-thread-cleanup! (current-thread) + (lambda () 'bar)) + 'foo)))) + (eq? (join-thread t) 'bar))) + + (pass-if "remove handler by setting false" + (let ((m (make-mutex))) + (lock-mutex m) + (let ((t (begin-thread (lock-mutex m) 'bar))) + (set-thread-cleanup! t (lambda () 'foo)) + (set-thread-cleanup! t #f) + (unlock-mutex m) + (eq? (join-thread t) 'bar)))) + + (pass-if "initial handler is false" + (not (thread-cleanup (current-thread))))))) [-- Attachment #3: Type: text/plain, Size: 143 bytes --] _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-18 0:41 ` Julian Graham 2007-10-20 11:12 ` Ludovic Courtès @ 2007-10-20 13:02 ` Ludovic Courtès 2007-10-20 22:19 ` Julian Graham 1 sibling, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-10-20 13:02 UTC (permalink / raw) To: guile-devel Hey, BTW, could you also provide a patch to update the documentation? Thanks in advance, :-) Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-20 13:02 ` Ludovic Courtès @ 2007-10-20 22:19 ` Julian Graham 2007-10-21 13:03 ` Ludovic Courtès 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-10-20 22:19 UTC (permalink / raw) To: Ludovic Courtès; +Cc: guile-devel Hi Ludovic, > BTW, could you also provide a patch to update the documentation? I'd be happy to -- I assume you're talking about stuff that's currently missing, like documentation for the condition variable functions? _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-20 22:19 ` Julian Graham @ 2007-10-21 13:03 ` Ludovic Courtès 2007-10-21 13:11 ` Ludovic Courtès 0 siblings, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-10-21 13:03 UTC (permalink / raw) To: guile-devel Hi Julian, "Julian Graham" <joolean@gmail.com> writes: > I'd be happy to -- I assume you're talking about stuff that's > currently missing, like documentation for the condition variable > functions? Indeed, I hadn't realized the documentation lacked this. Initially, I was referring to the new procedures you added, but adding condition variables, mutexes and friends would be nice too. Thanks, Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-21 13:03 ` Ludovic Courtès @ 2007-10-21 13:11 ` Ludovic Courtès 2007-10-23 14:16 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-10-21 13:11 UTC (permalink / raw) To: guile-devel ludo@gnu.org (Ludovic Courtès) writes: > Indeed, I hadn't realized the documentation lacked this. Initially, I > was referring to the new procedures you added, but adding condition > variables, mutexes and friends would be nice too. Actually, condition variables and mutexes seem to be fairly well documented, or did I miss something else? :-) Thanks, Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-21 13:11 ` Ludovic Courtès @ 2007-10-23 14:16 ` Julian Graham 2007-10-24 2:35 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-10-23 14:16 UTC (permalink / raw) To: Ludovic Courtès; +Cc: guile-devel > Actually, condition variables and mutexes seem to be fairly well > documented, or did I miss something else? :-) Oops, for some reason I was looking at the 1.6.8 manual (blame it on the cold I've had for the past few days). I'll have some docs in shortly. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-23 14:16 ` Julian Graham @ 2007-10-24 2:35 ` Julian Graham 2007-10-29 22:04 ` Ludovic Courtès 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-10-24 2:35 UTC (permalink / raw) To: Ludovic Courtès; +Cc: guile-devel [-- Attachment #1: Type: text/plain, Size: 459 bytes --] Find attached a patch that adds documentation for the thread cancellation functions. Let me know if I should change anything. On 10/23/07, Julian Graham <joolean@gmail.com> wrote: > > Actually, condition variables and mutexes seem to be fairly well > > documented, or did I miss something else? :-) > > > Oops, for some reason I was looking at the 1.6.8 manual (blame it on > the cold I've had for the past few days). I'll have some docs in > shortly. > [-- Warning: decoded text below may be mangled, UTF-8 assumed --] [-- Attachment #2: thread-cancellation-doc.HEAD.patch --] [-- Type: text/x-patch; name=thread-cancellation-doc.HEAD.patch, Size: 2538 bytes --] Index: api-scheduling.texi =================================================================== RCS file: /sources/guile/guile/guile-core/doc/ref/api-scheduling.texi,v retrieving revision 1.17 diff -a -u -r1.17 api-scheduling.texi --- api-scheduling.texi 17 Jun 2006 23:15:22 -0000 1.17 +++ api-scheduling.texi 24 Oct 2007 02:29:46 -0000 @@ -1,6 +1,6 @@ @c -*-texinfo-*- @c This is part of the GNU Guile Reference Manual. -@c Copyright (C) 1996, 1997, 2000, 2001, 2002, 2003, 2004 +@c Copyright (C) 1996, 1997, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007 @c Free Software Foundation, Inc. @c See the file guile.texi for copying conditions. @@ -285,6 +285,42 @@ immediate context switch to one of them. Otherwise, yield has no effect. @end deffn +@deffn {Scheme Procedure} cancel-thread thread +@deffnx {C Function} scm_cancel_thread (thread) +Asynchronously notify @code{thread} to exit. Immediately after receiving +this notification, @code{thread} will call its cleanup handler (if one +has been set) and then terminate, aborting any evaluation that is in +progress. + +Because Guile threads are isomorphic with POSIX threads, @code{thread} +will not receive its cancellation signal until it reaches a cancellation +point. See your operating system's POSIX threading documentation for +more information on cancellation points; note that in Guile, unlike +native POSIX threads, a thread can receive a cancellation notification +while attempting to lock a mutex. +@end deffn + +@deffn {Scheme Procedure} set-thread-cleanup! thread proc +@deffnx {C Function} scm_set_thread_cleanup_x (thread, proc) +Set @code{proc} as the cleanup handler for the thread @code{thread}. +@code{proc}, which must be a thunk, will be called when @code{thread} +exits, either normally or by being canceled. Thread cleanup handlers +can be used to perform useful tasks like releasing resources, such as +locked mutexes, when thread exit cannot be predicted. + +The return value of @var{proc} will be set as the @emph{exit value} of +@code{thread}. + +To remove a cleanup handler, pass @code{#f} for @code{proc}. +@end deffn + +@deffn {Scheme Procedure} thread-cleanup thread +@deffnx {C Function} scm_thread_cleanup (thread) +Return the cleanup handler currently installed for the thread +@code{thread}. If no cleanup handler is currently installed, +thread-cleanup returns @code{#f}. +@end deffn + Higher level thread procedures are available by loading the @code{(ice-9 threads)} module. These provide standardized thread creation. [-- Attachment #3: Type: text/plain, Size: 143 bytes --] _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-24 2:35 ` Julian Graham @ 2007-10-29 22:04 ` Ludovic Courtès 2007-10-29 22:20 ` Julian Graham 0 siblings, 1 reply; 23+ messages in thread From: Ludovic Courtès @ 2007-10-29 22:04 UTC (permalink / raw) To: guile-devel Hi Julian, "Julian Graham" <joolean@gmail.com> writes: > Find attached a patch that adds documentation for the thread > cancellation functions. Let me know if I should change anything. Great, I just installed it. A couple of nitpicks: - Make sure to use `@var', not `@code', for variables. - Leave two spaces after periods that end sentences. (See the GNU Coding Standards for more nitpicking. ;-)) Thanks! Ludo'. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-29 22:04 ` Ludovic Courtès @ 2007-10-29 22:20 ` Julian Graham 2007-10-29 23:23 ` Neil Jerram 0 siblings, 1 reply; 23+ messages in thread From: Julian Graham @ 2007-10-29 22:20 UTC (permalink / raw) To: Ludovic Courtès; +Cc: guile-devel Re: @var, @code, of course, my mistake. As for the spaces thing, both my parents are copy editors and I was raised to use the double space. But my girlfriend is also a copy editor (for newer, fancier magazines) and she assures me that the double space is a thing of the past. So my divergence from Guile's documentation style demonstrates... well, I don't know what it demonstrates, but I'll keep an eye on it for future submissions. ;) Thanks! On 10/29/07, Ludovic Courtès <ludo@gnu.org> wrote: > Hi Julian, > > "Julian Graham" <joolean@gmail.com> writes: > > > Find attached a patch that adds documentation for the thread > > cancellation functions. Let me know if I should change anything. > > Great, I just installed it. > > A couple of nitpicks: > > - Make sure to use `@var', not `@code', for variables. > > - Leave two spaces after periods that end sentences. > > (See the GNU Coding Standards for more nitpicking. ;-)) > > Thanks! > > Ludo'. > > > > _______________________________________________ > Guile-devel mailing list > Guile-devel@gnu.org > http://lists.gnu.org/mailman/listinfo/guile-devel > _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-29 22:20 ` Julian Graham @ 2007-10-29 23:23 ` Neil Jerram 2007-10-30 9:35 ` Ludovic Courtès 0 siblings, 1 reply; 23+ messages in thread From: Neil Jerram @ 2007-10-29 23:23 UTC (permalink / raw) To: Julian Graham; +Cc: Ludovic Courtès, guile-devel "Julian Graham" <joolean@gmail.com> writes: > Re: @var, @code, of course, my mistake. > > As for the spaces thing, both my parents are copy editors and I was > raised to use the double space. But my girlfriend is also a copy > editor (for newer, fancier magazines) and she assures me that the > double space is a thing of the past. But that could mean many things. For example, TeX doesn't care how many spaces you type in the source file; it will typeset the correct end-of-sentence-size space anyway. My understanding is that the two space convention is for fixed-width computer presentation, such as in Info. Many people seem to think that two spaces look better in that context. (Personally I always notice how browser-rendered text looks wrong in this respect - because HTML (like TeX) doesn't care how many spaces there are in the source, but the browsers (unlike TeX) don't leave a little extra width at the end of a sentence.) Regards, Neil _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
* Re: thread cancellation, take 2 2007-10-29 23:23 ` Neil Jerram @ 2007-10-30 9:35 ` Ludovic Courtès 0 siblings, 0 replies; 23+ messages in thread From: Ludovic Courtès @ 2007-10-30 9:35 UTC (permalink / raw) To: Neil Jerram; +Cc: guile-devel Hi, Neil Jerram <neil@ossau.uklinux.net> writes: > My understanding is that the two space convention is for fixed-width > computer presentation, such as in Info. Many people seem to think > that two spaces look better in that context. That's my understanding, too. It also helps differentiate periods that denote an abbreviation from periods at the end of sentences. Overall, I find it easier to read. Thanks, Ludovic. _______________________________________________ Guile-devel mailing list Guile-devel@gnu.org http://lists.gnu.org/mailman/listinfo/guile-devel ^ permalink raw reply [flat|nested] 23+ messages in thread
end of thread, other threads:[~2007-10-30 9:35 UTC | newest] Thread overview: 23+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2007-09-20 14:30 thread cancellation, take 2 Julian Graham 2007-09-20 15:18 ` Ludovic Courtès 2007-09-20 15:36 ` Julian Graham 2007-09-23 5:16 ` Julian Graham 2007-09-23 10:42 ` Ludovic Courtès 2007-09-23 18:39 ` Julian Graham 2007-09-24 11:42 ` Ludovic Courtès 2007-09-24 15:39 ` Julian Graham 2007-09-24 20:17 ` Julian Graham 2007-09-26 4:03 ` Ludovic Courtès 2007-09-27 2:39 ` Julian Graham 2007-10-18 0:41 ` Julian Graham 2007-10-20 11:12 ` Ludovic Courtès 2007-10-20 13:02 ` Ludovic Courtès 2007-10-20 22:19 ` Julian Graham 2007-10-21 13:03 ` Ludovic Courtès 2007-10-21 13:11 ` Ludovic Courtès 2007-10-23 14:16 ` Julian Graham 2007-10-24 2:35 ` Julian Graham 2007-10-29 22:04 ` Ludovic Courtès 2007-10-29 22:20 ` Julian Graham 2007-10-29 23:23 ` Neil Jerram 2007-10-30 9:35 ` Ludovic Courtès
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).