From: <me...@ho...> - 2005-09-25 19:58:59
|
I had a change of mind recently. It began with the realization that WITH-TIMEOUT as it is implemented in sbcl is inherently racy when nested: (handler-case (with-timeout 1 (handler-case (progn ;; the race is here (sleep 2) (with-timeout 2 ...)) (timeout () nil))) (timeout () nil)) Furthermore the solution described in my previous mails has nesting semantics that give me headaches. Maybe handling asynchronous conditions by the normal handlers is not a great idea? Allegro's WITH-TIMEOUT unwinds to the WITH-TIMEOUT form and runs the timeout forms. with-timeout (seconds &body timeout-body) &body body To implement it a catch can be set up that's thrown to by the interruption that's run by the timer. If that catch tag is 'private' (has gensym nature) then nesting WITH-TIMEOUTs is safe. The sketch of a solution I have in mind now is this: interruptions are run with a different set of condition handlers manipulated by ASYNCHRONOUS-HANDLER-BIND and put behind an unwind barrier. When a NLX tries to go through the barrier it is cancelled and the barrier returns normally (possibly warns or do other things). To get through the barrier an interruption needs a ticket. This ticket is established by CATCH/OOP (out-of-phase). The interruption throws to it by THROW/OOP and the barrier lets it through. Roughly. In more detail, when throwing THROW/OOP signals ABOUT-TO-THROW/OOP and the default handler looks up block reasons for async unwinding (established by WITH-ASYNCHRONOUS-UNWIND-BLOCK-REASON, cleanup forms or ungoing unwinds). If there is a block reason it signals BLOCKED-ASYNCHRONOUS-THROW for which the default handler invokes the RETRY-LATER restart and the throw (not the interruption!) is stored for the corresponding CATCH/OOP. Else it is let through the barrier. WITH-TIMEOUT is implemented by something like: (let ((tag (gensym "timeout-tag-"))) `(catch/oop ,tag (schedule-timer ,timeout (make-timer (lambda () (throw/oop ,tag)))) ,@body)) The timer function is called as an interruption (the IN-INTERRUPTION macro) meaning that: * it's wrapped in a barrier that blocks normal NLXs * lets oop throws through * binds *handler-clusters* & co to what ASYNCHRONOUS-HANDLER-BIND prescribed * sets up a handler for ABOUT-TO-THROW/OOP that checks the block reasons and signals BLOCKED-ASYNCHRONOUS-THROW Another important thing is block reasons only count if they are established after the CATCH/OOP tag. This approach is a whole lot nicer than the previous one. The main difference is that semantics don't change when a block reason is in effect (e.g. in cleanup forms). In other words nesting is sane. WITH-TIMEOUT works everywhere, including cleanup forms and interruptions themselves. Interruptions are out of phase wrt the normal control flow and the reality of the CL standard. Sharing condition handlers and allowing NLXs leads to trouble, but some kind of controlled tunnel has to be provided to travel between the two worlds. This is accomplished by CATCH/OOP and THROW/OOP with the help of IN-INTERRUPTION and WITH-ASYNCHRONOUS-UNWIND-BLOCK-REASON. TODO: * what if more than one throw is stored for a CATCH/OOP? which value(s) is returned? * better names * what does a barrier do when a NLX is denied? Note: Safe unwinding from foreign callbacks is likely to be implementable on top of catch/oop, throw/oop and unwind barriers with an in-callback macro thrown in that has a slightly different task from in-interruption but that's another story. Cheers, Gabor |