Version 6 of Coroutine cancellability

Updated 2022-03-20 04:18:47 by wusspuss

You can't generally cancel a running coroutine easily. You can of course do

rename runningCoro ""

But then:

  • the coroutine doesn't know it was cancelled(*), no error is raised inside it, it just ceases to exist
  • if the coroutine has scheduled itself as a callback like in e.g.
::chan event $chan readable [list [info coroutine]]

just like say coroutine::util gets does, then that event will produce an error. Unfortunately I found no way to handle coroutine cancellation without resorting to callbacks. The good news is one can do

proc cleanup {args} {...}
proc coro {} {
    ...
    trace add command [info coroutine] delete [list cleanup $arg1 $arg2]
    ...
}

Which will allow it to e.g. unsubscribe from fileevent. Quite awkward: another proc has to be created and it also has to accept mostly useless args that trace passes to it in addition to any useful args we may pass from inside the coro.

To sum up, here's one particular solution I could come up with:

package require coroutine::util
#let's say it's C for cancellable or coroutine 
namespace eval cio {}

proc cio::gets {args} {
    set chan [lindex $args 0]
    trace add command [info coroutine] delete [list cio::gets.cancel $chan]
    coroutine::util gets {*}$args
}

proc cio::gets.cleanup {chan args} {
    ::chan event $chan readable {}
}

This will not cause background errors should its caller be destroyed during the call. It's still sub-optimal: no error is raised in the caller, and if it wants to do some of its own cleanup, it's going to have to schedule other callbacks. Callbacks are harder to reason about than something like

proc coro {} {
    if {[catch {imaginary_cancellable_gets $chan} err]} {
        if {$err eq "cancelled"} {
            <cleanup>
        }    
    }
}

would be. But that's not possible to do with the current tcl coro mechanism, is it?

Now I found it's possible to use existing defer::defer package for this. Let's take coroutine::util after. An example that breaks it:

package require coroutine

proc main {} {
    coroutine::util after 12 
}

coroutine mainCoro main
after 1 mainCoro
vwait forever

Though coroutine finished due to outside factors earlier than it itself asked after to call it, after never knew the coroutine was finished. And we get

invalid command name "::mainCoro"
    while executing
"::mainCoro"
    ("after" script)

We can change tcllib/coroutine/coroutine.tcl with this small diff:

53d52
< package require defer
102,103c101
<     set id [::after $delay [list [info coroutine]]]
<     defer::defer after cancel $id
---
>     ::after $delay [list [info coroutine]]

And now the example runs smoothly, producing no error: after will no longer attempt the superfluous call.