Structured Concurrency in High-Level Languages
250bpm.com
250bpm.com
Cancellation isn't well-studied. It has two main cases - canceling a wait, and interrupting actual computation. The first case comes up more often.
If anything that can wait can be aborted, then things unwind better. Language support for this needs to be block-structured, as in Python's
Lock lok;
with lok :
(do something)
Python uses exceptions, so the appropriate mechanism there would be to indicate lock failure with an exception, and not execute the "with" block. In error code languages, the locking block should return an error code.Once a thread/coroutine/whatever is in cancellation, all waits should abort. So you rapidly fall through the code until the concurrent construct reaches its exit point. Unwinding still takes place. Non-blocking concurrency primitives should be allowed in this mode, so you can add things to a list, but can't wait on I/O. RAII things like file-closing on block exit have to be done that way, because the canceled construct can't wait for them.
QNX real-time messaging works something like this. QNX's basic interprocess communication primitive is MsgSend, which sends a message and waits for a reply. MsgSend can be canceled, or can time out; an error code is returned. All I/O requests are MsgSend operations and thus can be canceled. In QNX, everything that can block can be timed out or canceled. This is used to place upper bounds on how long things can take.
Canceling a compute loop is harder. If the compute loop is operating only on local data, it's not so bad. You need some data flow analysis to determine that the function being forced to error out doesn't have non-local side effects. If, say, you're inverting a matrix, and that needs to be aborted, you need to be sure that the matrix is never looked at again. This is probably easier in functional languages.
Once wait cancellation under control, so the thing you're canceling can't block, having to wait for the other thread/coroutine/whatever to reach its join point isn't so bad. You don't need to kill it; you can wait for it to unwind. So solving cancellation moots the problem of how to handle thread joins for failed threads.
These recent complaints about structured concurrency in popular languages to appear to shadow the complaints about call/cc vs delimited continuations. More info here: http://okmij.org/ftp/continuations/against-callcc.html.
declare
FOO BAR % FOO and BAR are not bound to any value yet.
in
thread
{print FOO} % the thread blocks here until FOO is bound to a value.
BAR = 456 % Binds BAR which wake up any thread waiting for it.
end
thread
FOO = 132 % Binds FOO which wake up any thread waiting for it.
{print BAR} % the thread blocks here until BAR is bound to a value.
end
Because of this, ordering of operations is easy. declare
FOO BAR % FOO and BAR are not bound to any value yet.
in
thread
{print FOO} % the thread blocks here until FOO is bound to a value.
BAR = 456 % Binds BAR which wake up any thread waiting for it.
end
thread
{print BAR} % the thread blocks here until BAR is bound to a value.
FOO = 132 % Binds FOO which wake up any thread waiting for it.
end
I swapped the instructions in the second thread. Does Oz detect that, or otherwise protect programmer from such mistakes?It's a deadlock which are also bad but much more deterministic and easier to debug.
However, if one is still free to do the moral equivalent of BAR = 1 or FOO = argc in a third thread, or even at compile/link time, then why should it a mistake to have these two threads blocking on such a (possible) event?
Off the top of my head:
* Cancel all threads in the bundle.
* Block until all threads in the bundle exit.
* Block until at least one thread in the bundle exits. Cancel all the other threads.
I think there also is one 'restart any exiting thread' and a variant of the block until all rule where only failed threads are restarted. Hmm, actually - perhaps there should be variants specifying the behavior of failed threads in all cases?It feels like Erlang/OTP is being reinvented right here again.
Once you get deep enough into concurrency the ideas start to converge on killable isolated processes with asynchronous message passing and programmable behaviors. Because with shared memory killing a thread has unpredictable consequences, you need to either enforce isolation or avoid consequences in some other way, like by not having multithreading and using an event loop. Isolated processes need to communicate with other processes somehow. You need message passing. But it has to be asynchronous to make it possible to implement various waiting behaviors. With event loops you get there via cancellable contexts and higher order programming, but on a single cpu. Getting into multi core or distributed systems yet again requires asynchronous message passing. At the end it's all actors everywhere.
With asynchronous messaging it's simple to build synchronous interactions if desired --- send a message and wait for a response; but you can't build asynchronous interactions on top of synchronous messages.
I thought the same thing. There are some cases like low message passing computational tasks where you can get away with simple async futures + fetch_future (being a computationally oriented language Julia encourages this model) but in the end actors are pretty great at generalizing concurrency.
A loom has the precise relation to actual warp threads as a virtual loom would to software (green) threads.
* Haskell and in particular Marlow's async library.
* The Erlang language's Actor system along with the OTP framework as a different way of thinking about orphaned processes (via supervision trees).
* The Pony language, which can safely pass ownership of objects (and thusly coroutines) around.
The academic world knew without experience that higher-level structured concurrency was the way to go. It took the practical industry decades of unmaintanable pthread messes to learn the same lesson, but they ultimately learned it.
Here's the general algorithm:
1. Find something interesting
2. Chase the references and read them.
3. Chase the citations (use Google Scholar) and read them, if they're relevant.
You'll pretty soon have an understanding of the general approaches and issues.
(If you throw "coroutine concurrency" into Google Scholar you'll very quickly find citations from the 80s discussing this model.)
It's a "middlebrow dismissal": http://www.byrnehobart.com/blog/why-are-middlebrow-dismissal...
It ignores the difference between theory and practice. Whether you do coroutines in C, Python, or another language matters. Language features interact, and those interactions affect how you write specific programs, so treating it abstractly has limits. Python actually has 2 completely different notion of coroutine, both of which were implemented after the 60's.
The ground isn't really well trodden, because most languages don't have coroutines. Probably 99% of programs don't use coroutines (nearly every program in C, C++, Java). Pre-emptive threads are more common.
You also didn't mention any literature which would have "said all the things".
That said, here is a good paper from 2004 from the authors of Lua: Revisiting Coroutines. Among other things, it points out that "coroutine" doesn't mean one thing.
https://scholar.google.com/scholar?cluster=14280883920152261...
Here are some of the relevant ideas and papers:
- "A coroutine is like a control construct"---a coroutine is a control construct. There is no "like". But there is a fundamental difference from a goto because it captures execution state (the stack and so on, also known as the continuation) which a goto does not. A continuation can only jump to program states that make sense (for some definition of "sense"---you can at least count on variables being initialised, for example). A goto can jump anywhere.
- "But coroutine is also like a variable"---a coroutine closes over an environment. I.e. it is a closure. This doesn't really have much to do with being "like a variable". Representing closures is well studied in programming language research and there are many techniques for handling it E.g. http://matt.might.net/articles/closure-conversion/
- "A coroutine can't be orphaned"---those are your continuations. You now need to worry about either heap allocating continuations or copying the stack. E.g. http://lampwww.epfl.ch/teaching/archive/advanced_compiler/20... And when do you free continuations? You probably need to GC them, as your control flow is now more complicated.
- "A coroutine can be orphaned"---this is where you need to think about semantics. What does it mean for a coroutine to be abruptly exited? It probably means leaking resources and hard to track down bugs. A lot of thought needs to go into this. (Somewhat relevant is this paper: https://www.cs.indiana.edu/~dyb/pubs/LaSC-7-1-pp83-110.pdf)
The rest of the article isn't so interesting to me, because I don't think thread cancellation should be possible at arbitrary times.
So that's some of the existing work in the field, but the biggest issue is the need to think about the semantics, or programming model, of the concurrent tools you're introducing. The article does very little of this. Can you actually understand programs written using some proposed concurrency primitives? That's where it really gets interesting, and where recent work like http://www.csc.kth.se/~phaller/doc/haller16-scala.pdf is making some thought provoking contributions.
Pedantic. A coroutine is a control construct in the same way an Exception is a control construct, or in the way a tomato or a cucumber is a fruit.
Adopting a particular naming depends on the categorization and the intention, not merely on the capabilities of a construct.
Even more so in a post, were the notion needs to be introduced to people who only associate the concept of a "control construct" to the more explicit and tamer constructs referred to as such, if and for loops, goto, etc.
>- "But coroutine is also like a variable"---a coroutine closes over an environment. I.e. it is a closure. This doesn't really have much to do with being "like a variable". Representing closures is well studied in programming language research and there are many techniques for handling it E.g. http://matt.might.net/articles/closure-conversion/*
The first part is just a formal definition, which doesn't negate the statement, and the latter part is just a generic reference to research on closures.
One could use the same kind of saying-nothing responses to all kinds of posts or papers.
You misunderstand.
If you know the name of the concept you can search for more information.
If you read the section on "But a coroutine is also like a variable" you'll see the author is struggling with how to represent coroutines. Closure conversion deals with representation.
Anything particular, besides a vast generalization?
One has a "nursery" while the other has a "bundle" with both hoping to control the lifetime of coroutines.
But yes, the author has a very similar approach to the problem.