"Asynchronous" exceptions, a subset of the features thread-interrupt!
would give us, break dynamic-wind guarantees:

(dynamic-wind
    (lambda () (increase! x))
    thunk
    (lambda () (decrease! x)))

Here, the idea is that within the dynamic extent of the THUNK, the
location of X should hold an increased value (see the sample
implementation of parameters in R7RS for a similar real-world
example).  This works if decrease! cannot fail.  This is no longer the
case with asynchronous exceptions (and first-class continuations).
Now one can argue that this is just the power of thread-interrupt! and
it is the programmer's responsibility not to misuse it (a point of
view every C programmer could subscribe to); however, dynamic-wind was
made to uphold the concept of a dynamic environment even in the
presence of non-local control flow.

Thus, if we have asynchronous exceptions (or, more generally,
thread-interrupt!), we probably need a parameter-like object
INTERRUPT-LEVEL, a non-negative integer.  If INTERRUPT-LEVEL is
positive, interrupts will be postponed.  Dynamic-wind would then be
modified so that it is in itself atomic, and it would reparameterize
the INTERRUPT-LEVEL (so that it increases) during the execution of
BEFORE and AFTER.  A possible definition would be

(define dynamic-wind
  (lambda (before thunk after)
    (with-interrupts-disabled
      (unmodified-dynamic-wind
        before
        (lambda () (with-interrupts-enabled (thunk)))
        after))))

Here, with-interrupts-enabled/disabled does the reparameterizing
increasing/decreasing INTERRUPT-LEVEL by one.

An alternative would be not to make the modified dynamic-wind the
official dynamic-wind but the unmodified one, which would probably
lead to unsafer code in the wild.  Note that the unmodified
dynamic-wind can also be expressed in the modified dynamic-wind:

(define unsafe-dynamic-wind
  (lambda (before thunk after)
    (safe-dynamic-wind
      (lambda () (with-interrupts-enabled (before))
      thunk
      (lambda () (with-interrupts-enabled (after))))))

Thoughts?

PS: Thread-emergency-exit (or whatever it will be called) would still
bypass all dynamic-winds. But could only be harmful for data shared
across threads and they have to be carefully treated with mutexes
anyway.

Am Di., 8. Nov. 2022 um 16:57 Uhr schrieb Marc Nieper-Wißkirchen
<[email protected]>:
>
> Am Di., 8. Nov. 2022 um 16:01 Uhr schrieb Marc Feeley 
> <[email protected]>:
> >
> >
> > > On Nov 8, 2022, at 2:27 AM, Marc Nieper-Wißkirchen 
> > > <[email protected]> wrote:
> > >
> > > Am Mo., 7. Nov. 2022 um 21:08 Uhr schrieb Marc Feeley 
> > > <[email protected]>:
> > >>
> > >>
> > >>> On Nov 7, 2022, at 1:43 PM, Marc Nieper-Wißkirchen 
> > >>> <[email protected]> wrote:
> > >>>
> > >>> Thank you!
> > >>>
> > >>> Am Mo., 7. Nov. 2022 um 19:19 Uhr schrieb Marc Feeley 
> > >>> <[email protected]>:
> > >>>>
> > >>>>
> > >>>>> On Nov 7, 2022, at 4:41 AM, Marc Nieper-Wißkirchen 
> > >>>>> <[email protected]> wrote:
> > >>>>>
> > >>>>> Hey Marc,
> > >>>>>
> > >>>>> could you describe the exact semantics of Gambit's thread-interrupt!
> > >>>>> or give me a link to where it is documented?
> > >>>>>
> > >>>>> Specifically, what happens when the thunk returns normally?
> > >>>>>
> > >>>>> We talked about capturing a continuation inside the thunk, which is
> > >>>>> related to the question.
> > >>>>>
> > >>>>> Thanks,
> > >>>>>
> > >>>>> Marc
> > >>>>>
> > >>>>
> > >>>> The API and semantics of thread-interrupt! has evolved over time since 
> > >>>> the introduction of threads in Gambit v4.0 (~2000).  The basic idea is 
> > >>>> to force a runnable or blocked thread to immediately execute a call to 
> > >>>> a thunk, regardless of what that thread is currently doing.  
> > >>>> Conceptually each thread executes a series of atomic actions (“atomic” 
> > >>>> in the sense that they are operations at a certain level of 
> > >>>> abstraction of the virtual machine, such as a call to “cons”, “car”, 
> > >>>> “pair?”, etc).  But note that some Scheme predefined procedures, such 
> > >>>> as “append”, “map”, etc are not atomic and are a series of atomic 
> > >>>> actions.  The thread interrupt mechanism inserts the call to the thunk 
> > >>>> between such atomic actions thus ensuring that interrupts happen at 
> > >>>> “safe places” (for the Scheme virtual machine, which does not mean 
> > >>>> that it is safe for the logic of the program, which is the 
> > >>>> programmer’s concern).  Conceptually, if the thread was about to 
> > >>>> evaluate <expr>, it replaces this by the evaluation of (begin (thunk) 
> > >>>> <expr>) so that the thunk’s result is ignored and the thunk is called 
> > >>>> with the thread’s current continuation as a parent, including the 
> > >>>> dynamic environment.
> > >
> > > The problem I still see is that thread-interrupt! and, to a lesser
> > > extent, thread-exit! will expose implementation details of standard
> > > procedures (those that are implemented as non-atomic ones).
> > >
> > > Assume that we have a procedure "foo" in the standard, that, by
> > > definition, increases the global variable "x" by one and then the
> > > global variable "y" before it returns.
> > >
> > > With the usual "as if" rule, it wouldn't matter if foo were implemented as
> > >
> > > (define foo
> > >  (lambda ()
> > >    (set! x (+ x 1))
> > >    (set! y (+ y 1))))
> > >
> > > or as
> > >
> > > (define bar
> > >  (lambda ()
> > >    (set! y (+ y 1))
> > >    (set! x (+ x 1))))
> > >
> > > With thread-interrupt!, however, a continuation in the middle of the
> > > execution of foo could be caught, making the order of assignments
> > > observable.  This would suddenly rule out the second implementation
> > > because the specification of "foo" would have become an
> > > over-specification with the introduction of thread-interrupt!.
> > >
> > > Even more complicated is the matter with the non-atomic increase of x and 
> > > y.
> >
> > But this issue (exposing the implementation) is already the case currently:
> >
> > 1) when the operation throws an exception and the exception handler 
> > captures the continuation (for example imagine the variable x in (set! x (+ 
> > x 1)) contains a non-number and “+” raises a type exception, or that x is a 
> > bignum and “+” needs to allocate a bignum and this needs to call the 
> > garbage collector because the heap is full, and the GC “hook” (a common 
> > very useful extension) captures the continuation to show the user where the 
> > GC was triggered or for profiling).
>
> My example "bar" procedure is probably not a good example.  A standard
> library routine would probably first check whether "x" and "y" are
> numbers and would employ mutexes if "x" and "y" are not thread-local.
> I am less concerned about a GC hook unless such one is standardized as
> well.
>
> > 2) when the operation includes a procedure call such as (set! x (f x)) 
> > where f might contain code that captures the continuation or raises an 
> > exception like for #1
>
> Such procedures would indeed have to be coded carefully to match the
> spec.  We discussed this on the SRFI 231 mailing list (and used
> `vector-map' as an example, if my memory serves me right).  (Related
> to this are my recent messages on the SRFI 127 and SRFI 158 mailing
> lists.)
>
> > 3) when “+” has been set! to a procedure that captures the continuation 
> > (this may not be allowed by R7RS, but it is in R5RS and in Scheme systems 
> > that allow this for debugging reasons)
>
> At least in R5RS, standard library procedures would not be affected.
> If it is a non-standard extension, I don't see this as a problem as a
> non-standard extension should be allowed to inspect the VM even on the
> level of electrons and nucleons.
>
> > Moreover it is possible using threads to observe y being mutated before x, 
> > so the transformation you mention above is limited to non-threaded code so 
> > clearly not in situations where thread-interrupt! would be used.
>
> See above; every example code has limits.  So let us assume that x and
> y are lexically scoped but that they can be accessed through getters
> outside of "bar".
>
> > If it is critical that the two assignments can’t be interrupted in the 
> > middle then this should be explicitly enforced with a critical section.  
> > This could be achieved using a boolean parameter object that indicates if 
> > interrupts should be handled immediately or deferred until later:
> >
> >     (define bar
> >       (lambda ()
> >         (parameterize ((defer-interrupts? #t))
> >           (set! x (+ x 1))
> >           (set! y (+ y 1)))))
> >
> > When defer-interrupts? is #t any incoming interrupts are put in a queue.  
> > When defer-interrupts? goes from #t to #f the interrupts on the queue are 
> > serviced.
> >
> > It could also be a special form (with-deferred-interrupts thunk) to hide 
> > the parameter object, or equivalent mechanism.
>
> I have been thinking about this; Chez Scheme has something similar,
> and on the (x86) machine level, the cli/sti instructions do the same.
>
> I don't entirely like it; for simplicity, programmers may just wrap
> the entire code in a critical section.  And if an implementation's GC
> works through the interrupt mechanism, disabling interrupts would be
> problematic in any case.  (But, maybe, the latter should be solved
> independently by such an implementation.)
>
> >
> > >
> > > Another problem is that the application, including the standard
> > > libraries (but not the VM) may be in an unsafe state at the time of
> > > the interrupt.  So any call to a standard procedure may crash the
> > > system.  But the standard should allow an implementation to offer a
> > > safe mode.
> >
> > This is why I say at that point it is the programmer’s responsibility to 
> > use this powerful construct correctly (similarly to using first class 
> > continuations and assignments together).
>
> If the standard libraries are written safely, and the VM can operate
> in a safe mode, I cannot crash the system (I am not talking about the
> application!) using first-class continuations or assignments.
>
> My problem is that with thread-interrupt!, it seems to become very
> hard to write safe standard library implementations (where "safe" is
> in the above sense).
>
> >
> > >
> > > Hypothetical procedures "thread-raise" and "thread-raise-continuable",
> > > would make it a bit easier.  We would then have:
> > >
> > > (define thread-interrupt!
> > >  (lambda (thread thunk)
> > >    (thread-raise-continuable thread (make-interrupt-condition (lambda
> > > (exc) (thunk)))))
> > >
> > > (define-condition-type &interrupt-condition &condition
> > >  make-interrupt-condition interrupt-condition?
> > >  (handler interrupt-condition-handler))
> > >
> > > The initial exception handler would then be:
> > >
> > > (lambda (exc)
> > >  (cond
> > >    [(interrupt-condition? exc)
> > >     ((interrupt-condition-handler) exc)]
> > >    [else ...]))
> > >
> > > Procedures like foo could then install a custom exception handler
> > > ("interrupt handler").
> > >
> > > This would make thread-interrupt! less powerful, though.
> > >
> > > What is a good way out?
> >
> > Isn’t it still possible to write an exception handler (called at the moment 
> > of an interrupt) that contains a continuation capture?  So I’m not sure 
> > what is gained by doing it this way.
>
> A critical section could install an exception handler that first
> unwinds to the beginning of the critical section (which includes
> resetting the continuation to the beginning) before it calls the
> interrupt thunk in this unwound continuation.
>
> The problem is, of course, that a second interrupt may arrive during
> the unwinding process, and this interrupt would see an exception
> handler up the stack...
>
> > On a separate subject I have some thoughts about thread-exit!.  I think 
> > this procedure should drop the “!” for consistency with the R7RS “exit” 
> > procedure.  The semantics should also be modeled after “exit” and 
> > “emergency-exit”:
>
> What was your rationale for "!" vs no "!" in SRFI 18?
>
> >    (thread-exit [obj])
> >    (thread-emergency-exit [obj])
> >
> > in such a way that in the primordial thread we have:
> >
> >    (exit [obj]) = (thread-exit [obj])
> >    (emergency-exit [obj]) = (thread-emergency-exit [obj])
> >
> > The thread-emergency-exit procedure would “Terminate the thread without 
> > running any outstanding dynamic-wind after procedures” (same wording as 
> > “emergency-exit” but “program” is replaced by “thread”).  The optional 
> > “obj” parameter would be the thread’s result, accessible with (thread-join! 
> > <thread>).
>
> If, <obj> should be a "reason" of the exception that is currently
> raised by joining an abnormally terminated thread, I think.
>
> If we have something like thread-interrupt!, I don't think we need
> something like "emergency-exit".  It is enough to throw an exception
> (that would finally be handled by the initial exception handler).
>
> >
> > A thread-terminate! procedure would not be needed because a thread could 
> > terminate a target thread with either:
> >
> >    (thread-interrupt! <target-thread> thread-exit)
> >    (thread-interrupt! <target-thread> thread-emergency-exit)
> >
> > and the first form would be preferred to let the thread do any required 
> > cleanup.  It would also be possible to do (thread-interrupt! 
> > <target-thread> thread-exit), then (thread-join! <target-thread> 
> > <timeout>), followed by (thread-interrupt! <target-thread> 
> > thread-emergency-exit) if the timeout was reached, in case the target 
> > thread is wedged.
> >
> > Marc
> >
> > >
> > > [...]
> > >
> > >>>
> > >>> Does Gambit handle signals through thread-int!, e.g. as in the 
> > >>> following?
> > >>>
> > >>> (##thread-int! <thread> (lambda () (raise-continuable 
> > >>> <signal-condition>)))
> > >>>
> > >>> or
> > >>>
> > >>> (##thread-int! <thread> (lambda () (signal-interrupt <info>)))
> > >>>
> > >>> where
> > >>>
> > >>> (define (signal-interrupt info)
> > >>> ((current-signal-interrupt-handler) info))
> > >>
> > >> It depends what you mean by “a signal”…  If you mean something related 
> > >> to POSIX signals then no it is not implemented that way because POSIX 
> > >> signals are executed asynchronously (with respect to the Gambit virtual 
> > >> machine) so they could happen in the middle of some VM operation that 
> > >> should not be interrupted because the VM state is temporarily 
> > >> inconsistent.  So instead a POSIX signal will register the signal in a 
> > >> bit set, and then raise an “interrupt flag” that is checked regularly at 
> > >> “safe points” and the handler checks the bit set.  This is a very low 
> > >> overhead polling mechanism that piggybacks on the stack overflow 
> > >> detection logic.  See my paper:
> > >
> > > I meant a "VM signal", which could be triggered by a POSIX signal. I
> > > was interested in how the high-level interface/high-level semantics
> > > worked.  It seemed to me that thread-interrupt! is a sufficiently
> > > general primitive.
> > >
> > >>   Polling Efficiently on Stock Hardware, FPCA93 
> > >> (http://www.iro.umontreal.ca/~feeley/papers/FeeleyFPCA93.pdf)
> > >
> > > I have been experimenting a bit with virtual machines.  There, I
> > > usually use hardware detection for a stack overflow.  The overflow
> > > handler expands the stack but then sets an interrupt flag.  The actual
> > > code then just has to poll the interrupt flag (some volatile
> > > sig_atomic_t).
> > >
> > >> Fun fact: that paper has achieved a certain notoriety because it is one 
> > >> of the few references in the book “The Java Language Specification” in 
> > >> the section 11.3.2 “Handling Asynchronous Exceptions”.
> > >
> > > Cool!
> > >
> > > Marc
> > >
> > > [...]
> >
> >

Reply via email to