[Sbcl-devel] Re: [Openmcl-devel] async signals

Gábor Melis mega at hotpop.com
Sun Sep 25 13:03:03 PDT 2005


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))))
      , at 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



More information about the Openmcl-devel mailing list