How to yield a thread's current continuation from an exception handler

Viewed 173

This code is really pushing the limits of my understanding so bear with me.

Previously I implemented coroutines in Racket in the following code:

;; Coroutine definition  
(define (make-generator procedure)
  (define last-return values)
  (define last-value #f)
  (define status 'suspended)
  (define (last-continuation _) 
    (let ([result (procedure yield)]) 
      (last-return result)))

  (define (yield value)
    (call/cc (lambda (continuation)
               (set! last-continuation continuation)
               (set! last-value value)
               (set! status 'suspended)
               (last-return value))))

  (lambda args
    (call/cc (lambda (return)
               (set! last-return return)
               (cond ((null? args) 
                      (let ()
                        (set! status 'dead)
                        (last-continuation last-value)))
                     ((eq? (car args) 'coroutine?) 'coroutine)
                     ((eq? (car args) 'status?) status)
                     ((eq? (car args) 'dead?) (eq? status 'dead))
                     ((eq? (car args) 'alive?) (not (eq? status 'dead)))
                     ((eq? (car args) 'kill!) (set! status 'dead))
                     (#t (apply last-continuation args)))))))


;;Define a function that will return a suspended coroutine created from given args and body forms
(define-syntax (define-coroutine stx)
  (syntax-case stx ()
               ((_ (name . args) . body )
                #`(define (name . args)
                    (make-generator
                      (lambda (#,(datum->syntax stx 'yield))
. body))))))

What I want to do is implement an exception handler (with-handlers) that calls the (yield) function. The idea is a second thread can send a signal to the thread evaluating the coroutine forcing it to yield when its running for too long.

I've tried the following in the args lambda, which successfully returned early but later evaluations of the coroutine (my-coroutine 'dead?) returned that the coroutine was in the 'dead state:

                (with-handlers 
                  ([exn:break? 
                     (lambda (break)
                       (yield 'coroutine-timeout))])
                  (break-enabled #t) ;register for yield requests from coroutine manager thread
                  (last-continuation last-value))))

Alternatively, I've tried the following, but it didn't produce a procedure that can be applied to arguments:

                (with-handlers 
                  ([exn:break? 
                     (lambda (break)
                       (set! last-continuation (exn:break-continuation break))
                       (set! last-value 'coroutine-timeout)
                       (set! status 'suspended)
                       (last-return 'coroutine-timeout))])
                  (break-enabled #t) ;register for yield requests from coroutine manager thread
                  (last-continuation last-value))))

I'm trying to understand how continuations and exceptions interact/block each other. It seems like I may need to use Parameters somehow?

How can I successfully write a signal handler that will (yield) correctly so that I can resume the coroutine later?

Edit: I am mixing metaphores here (cooperative and preemptive multithreading). However, my question seems possible to me (from a layman's perspective) as I can evaluate functions defined in my coroutine (including (yield)) from within the exception handler. I'm essentially trying to limit resource starvation in my worker threads, as well as mitigate a certain class of deadlock (where task 1 can only complete after task 2 has run, and there are no free threads for task 2 to run on).

I have written a (go) function for these coroutines that is modeled after go's goroutines. I assume they achieve their asynchronous behavior on single threads by having cooperative yield checks in the underlying code they control. Perhaps it runs in a VM as you suggested and there are checks, perhaps their operators have the checks. Whatever the case may be I'm trying to achieve similar behavior with a different strategy.

2 Answers

As far as "how continuations and exceptions interact/block each other," it's important to know that exceptions are implemented using delimited continuations. In particular, the exception system makes use of continuation barriers. Both of these are introduced in the Racket reference §1.1.12 Prompts, Delimited Continuations, and Barriers:

A continuation barrier is another kind of continuation frame that prohibits certain replacements of the current continuation with another. … A continuation barrier thus prevents “downward jumps” into a continuation that is protected by a barrier. Certain operations install barriers automatically; in particular, when an exception handler is called, a continuation barrier prohibits the continuation of the handler from capturing the continuation past the exception point.

You may also want to see the material on exceptions from later in the evaluation model section and from the control flow section, which cites an academic paper on the subject. The differences between call-with-exception-handler and with-handlers are also relevant to capturing continuations from within exception handlers.

Basically, though, the continuation barrier prevents using exception handlers for continuations that you abort and might later resume: you should use continuation barriers and prompts directly for that.

More broadly, I would suggest that you look at Racket's substantial existing support for concurrency. Even if you want to implement coroutines as an experiment, they would be useful for inspiration and examples of implementation techniques. Racket comes with derived constructs such as engines ("processes that can be preempted by a timer or other external trigger") and generators, in addition to the fundamental building-blocks, green threads and synchronizable events (which are based on Concurrent ML model).

The gist of your question:

How can I implement an exception handler for coroutines, such that a second thread can send a signal to a thread evaluating a coroutine, forcing it to yield when its running for too long.

And once more:

How can I successfully write a signal handler that will (yield) correctly so that I can resume the coroutine later?

It seems to me that you are not cleanly separating cooperative and preemptive multitasking, since you seem to want to combine coroutines (cooperative) with time-outs (preemptive). (You also mention threads, but seem to conflate them with coroutines.)

With cooperative multitasking there is no way that you can force anyone else to stop running; hence the moniker "cooperative".

With preemptive multitasking you do not need to yield, because the scheduler will preempt you when your allocated time has run out. The scheduler is also responsible for saving your continuation, but it is not the (scheduler's) current continuation, since the scheduler is wholly separate from the user thread.

Perhaps the closest thing to what you are proposing is simulating preemptive multitasking via polling. Every (simulated) timestep (i.e. a VM instruction) the simulation needs to check whether any interrupts/signals have been received by a running thread and handle them.

Related