Notes on structured concurrency, or: Go statement considered harmful

Redoubts1 pts0 comments

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...

myfunc goto like statement nurseries nursery

Related Articles