Some background: conceptually, there are three states a scope can be in:
- "open", allowing new threads to be created within it
- "closing", because we reached the end of a
scoped block naturally, or because we got hit by an exception, and are going to proceed to killing all living children and waiting for them to terminate
- "closed", because we're outside the callback in which the scope was valid, like any regular resource acquired in bracket-style
Clearly, we do want to disallow this bogus program, either with the type system (meh) or via a runtime exception:
scope <- Ki.scoped pure
Ki.fork scope whatever -- using a scope outside its callback
On to the implementation. Each scope keeps an int count of the threads that are about to start, with the sentinel value -1 meaning closed/closing. When we go to fork a thread, if this counter is not -1, we bump the counter, then spawn the thread, then decrement the counter. If the counter is -1, we throw a runtime exception (error "ki: scope closed").
This design makes closing a scope pretty simple: wait until there are 0 children about to start, then prevent new children from starting by writing -1, then kill all of the living children.
The problem (potentially) is that there's not actually a "closing" state that's distinguishable from "closed". So while we do prevent bogus programs like the above from spawning a thread in a closed scope, it seems wrong to punish code that attempts to spawn a thread into a closing scope in the same way.
Some options:
- (straw man) Make
fork have type fork :: Scope -> IO a -> IO (Maybe (Thread a)), and return Nothing if we try to fork a thread in a closing scope. I don't think this API is good, but it's conceptually what we are after. The current behavior (to reiterate/summarize the above) is to throw a lazy runtime exception with error "ki: scope closing" rather than return Nothing.
- Make
Thread a two-variant sum type, with a DidntActuallyMakeTheThreadBecauseTheScopeWasClosing variant. We'll have to decide what to do if you await such a thing.
- Tweak the teardown dance to actually continue to allow threads to be created in a closing scope, if only to throw a
ScopeClosing exception to them soon after (which is how we kill children). This doesn't seem meaningfully different to (2).
- Something else, or nothing?
Some background: conceptually, there are three states a scope can be in:
scopedblock naturally, or because we got hit by an exception, and are going to proceed to killing all living children and waiting for them to terminateClearly, we do want to disallow this bogus program, either with the type system (meh) or via a runtime exception:
On to the implementation. Each scope keeps an int count of the threads that are about to start, with the sentinel value -1 meaning closed/closing. When we go to fork a thread, if this counter is not -1, we bump the counter, then spawn the thread, then decrement the counter. If the counter is -1, we throw a runtime exception (error "ki: scope closed").
This design makes closing a scope pretty simple: wait until there are 0 children about to start, then prevent new children from starting by writing -1, then kill all of the living children.
The problem (potentially) is that there's not actually a "closing" state that's distinguishable from "closed". So while we do prevent bogus programs like the above from spawning a thread in a closed scope, it seems wrong to punish code that attempts to spawn a thread into a closing scope in the same way.
Some options:
forkhave typefork :: Scope -> IO a -> IO (Maybe (Thread a)), and return Nothing if we try to fork a thread in a closing scope. I don't think this API is good, but it's conceptually what we are after. The current behavior (to reiterate/summarize the above) is to throw a lazy runtime exception witherror "ki: scope closing"rather than return Nothing.Threada two-variant sum type, with aDidntActuallyMakeTheThreadBecauseTheScopeWasClosingvariant. We'll have to decide what to do if youawaitsuch a thing.ScopeClosingexception to them soon after (which is how we kill children). This doesn't seem meaningfully different to (2).