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.