Notes on structured concurrency, or: Go statement considered harmful — njs blog
fake link
wrapped<br>around the , but I don't have one of those --><br>Every concurrency API needs a way to run code concurrently. Here's<br>some examples of what that looks like using different APIs:
go myfunc(); // Golang
pthread_create(&thread_id, NULL, &myfunc); /* C with POSIX threads */
spawn(modulename, myfuncname, []) % Erlang
threading.Thread(target=myfunc).start() # Python with threads
asyncio.create_task(myfunc()) # Python with asyncio
There are lots of variations in the notation and terminology, but the<br>semantics are the same: these all arrange for myfunc to start<br>running concurrently to the rest of the program, and then return<br>immediately so that the parent can do other things.
Another option is to use callbacks:
QObject::connect(&emitter, SIGNAL(event()), // C++ with Qt<br>&receiver, SLOT(myfunc()))
g_signal_connect(emitter, "event", myfunc, NULL) /* C with GObject */
document.getElementById("myid").onclick = myfunc; // Javascript
promise.then(myfunc, errorhandler) // Javascript with Promises
deferred.addCallback(myfunc) # Python with Twisted
future.add_done_callback(myfunc) # Python with asyncio
Again, the notation varies, but these all accomplish the same thing:<br>they arrange that from now on, if and when a certain event occurs,<br>then myfunc will run. Then once they've set that up, they<br>immediately return so the caller can do other things. (Sometimes<br>callbacks get dressed up with fancy helpers like promise<br>combinators,<br>or Twisted-style protocols/transports,<br>but the core idea is the same.)
And... that's it. Take any real-world, general-purpose concurrency<br>API, and you'll probably find that it falls into one or the other of<br>those buckets (or sometimes both, like asyncio).
But my new library Trio is weird. It<br>doesn't use either approach. Instead, if we want to run myfunc and<br>anotherfunc concurrently, we write something like:
async with trio.open_nursery() as nursery:<br>nursery.start_soon(myfunc)<br>nursery.start_soon(anotherfunc)
When people first encounter this "nursery" construct, they tend to<br>find it confusing. Why is there an indented block? What's this<br>nursery object, and why do I need one before I can spawn a task?<br>Then they realize that it prevents them from using patterns they've<br>gotten used to in other frameworks, and they get really annoyed. It<br>feels quirky and idiosyncratic and too high-level to be a basic<br>primitive. These are understandable reactions! But bear with me.
In this post, I want to convince you that nurseries aren't quirky or<br>idiosyncratic at all, but rather a new control flow primitive that's<br>just as fundamental as for loops or function calls. And furthermore,<br>the other approaches we saw above – thread spawning and callback<br>registration – should be removed entirely and replaced with<br>nurseries.
Sound unlikely? Something similar has actually happened before: the<br>goto statement was once the king of control flow. Now it's a<br>punchline. A few languages still have<br>something they call goto, but it's different and far weaker than<br>the original goto. And most languages don't even have that. What<br>happened? This was so long ago that most people aren't familiar with<br>the story anymore, but it turns out to be surprisingly relevant. So<br>we'll start by reminding ourselves what a goto was, exactly, and<br>then see what it can teach us about concurrency APIs.
Contents:
What is a goto statement anyway?
What is a go statement anyway?
What happened to goto?<br>goto: the destroyer of abstraction
A surprise benefit: removing goto statements enables new features
goto statements: not even once
go statement considered harmful<br>go statements: not even once
Nurseries: a structured replacement for go statements<br>Nurseries preserve the function abstraction.
Nurseries support dynamic task spawning.
There is an escape.
You can define new types that quack like a nursery.
No, really, nurseries always wait for the tasks inside to exit.
Automatic resource cleanup works.
Automated error propagation works.
A surprise benefit: removing go statements enables new features
Nurseries in practice
Conclusion
Comments
Acknowledgments
Footnotes
What is a goto statement anyway?
Let's review some history: Early computers were programmed using<br>assembly language, or other even<br>more primitive mechanisms. This kinda sucked. So in the 1950s, people<br>like John Backus at<br>IBM and Grace Hopper<br>at Remington Rand started to develop languages like FORTRAN and FLOW-MATIC (better known for its<br>direct successor COBOL).
FLOW-MATIC was very ambitious for its time. You can think of it as<br>Python's great-great-great-...-grandparent: the first language that<br>was designed for humans first, and computers second. Here's some<br>FLOW-MATIC code to give you a taste of what it looked like:
You'll notice that unlike modern languages, there's no if blocks,<br>loop blocks, or function calls here – in fact there's no block<br>delimiters or indentation at all. It's...