
Notes on structured concurrency, or: Go statement considered harmful - m0meni
https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
======
arghwhat
What.

This article proposes a "nursery", which is just a wrapped
sync.WaitGroup/pthread_join/futures::future::join_all/a reactor that waits for
all tasks to terminate/etc.

It then uses an exception-like model for error propagation to "solve" error
handling (which is fairly easy to handle with a channel).

The construct is a decently usable, already applied tool to handle a set of
problems, but the article takes the issue way out of proportions and overhypes
the proprosed solution. The "with" example for benefits to not having a "go"
statement seem rather bogus, especially seeing that such RAII constructs do
not exist in Go (no destructors, remember?).

Trying to claim that "go" is as terrible as the original "goto" is ignorance
of the original problems. Bad use of goto can be a nightmare to track (as the
author tried to illustrate), but goroutines do not jump around, they _branch_
from the main goroutine, following normal control flow from there. They are
easy to follow, and the language is designed so that you can throw around with
them and forget them without them causing you problems.

Also, this article is comparing a list of concurrency constructs and one
parallism construct (pthread_create—threading.Thread doesn't count as
parallism due to GIL) to _callbacks_ , something which have nothing to do with
concurrency _at all_. Very odd.

~~~
scott_s
I thought the article's comparison of go routines to goto was fair, in the
context made: when calling a function in a language that allows go-routine
like things (including pthread_create), one can't know if background tasks
will be spawned. This is similar in the free-form time of unrestricted goto
that one could not know if control flow really would return to the point
lexically after the function call. His proposed abstraction does allow one to
look after a nursery block and know whether or not background tasks are still
running.

~~~
arghwhat
Knowing whether a background task has spawned is very different from not being
able to follow the control flow (goto potentially jumping to an entirely
different function body).

Now, while Go is designed mostly for you to not _care_ about goroutines, there
are some corner cases where one must know if a _resource_ is used by anything,
such as the chase of when you wish to close a file handle.

However, I'd argue that this is not related to go's concurrency model and the
presence of background tasks at all. It's related to object lifetimes. This
should be made clear at API surfaces.

A language solution to this would be Rust's lifetimes, not a new concurrency
model.

~~~
marcus_holmes
But this concept does also solve that problem. Maybe more simply.

Rust is renowned for being a total headache with borrowing and lifetimes.
Nurseries might be a simpler solution for this.

As you say, a corner case. But a common one...

~~~
lilyball
Nurseries and lifetimes are orthogonal concepts. In fact, Rust _already has_ a
"nursery", except it's generally called a scoped threadpool. They use
lifetimes to ensure the threads can reference stuff on the stack without
copying it. So a "nursery" is basically just a scoped threadpool with a
'static lifetime.

~~~
stouset
I don’t think there’s any reason a nursery has to have a static lifetime.

~~~
eridius
The description of the nursery is you can pass it around wherever you like.
More generally, this concept seems to have been designed in a language without
lifetimes. You obviously can have a scoped threadpool without a 'static
lifetime, but if you want to pass it around to arbitrary locations then you
do.

~~~
twic
I think the whole point of the nursery's design is that you can create and
destroy them during the life of your program, and that destroying them forms a
barrier where the owner of the nursery waits for its children to finish.
Having a nursery with a static lifetime in Rust would therefore be pointless,
as it would live until the end of the program.

~~~
eridius
I think you misunderstand. I'm talking about writing something like
`ScopedThreadpool<'static>`, which means any external data referenced by the
threadpool must have a static lifetime (or rather, it means the
ScopedThreadpool cannot reference anything on the stack, because that would
prevent you from e.g. returning it to your caller or passing it to another
thread). The ScopedThreadpool itself can be created and destroyed whenever.

------
erpellan
The author appears to have reinvented Communicating Sequential Processes
(CSP).

[http://www.usingcsp.com/cspbook.pdf](http://www.usingcsp.com/cspbook.pdf)

The (simplified, and as I understand it) gist of concurrency in CSP is that
the program is expressed as a series of parallel (PAR) and sequential (SEQ)
operations.

Everything in a PAR block will run in parallel and all their outputs will be
collected and fed as the input to the next SEQ. Everything in a SEQ will run
sequentially as a pipeline until the next PAR. Every PAR must follow a SEQ and
vice versa, as two PARS or SEQS next to each other will simply coalesce.

eg.

    
    
        PAR
          longCall1
          longCall2
          longCall3
        SEQ
          reduceAllThreeResults
          doSomethingWithTheReducedResult
        PAR
          nextParallelOp1
          nextParallelOp2
    

etc.

~~~
m0meni
This misses the point of the article. It's short-sighted to say that he's
reinvented CSP given that the entire concurrency model in Go is based around
CSP, and the author is already aware of it. The article is more related to
RAII, scope, lifetimes, etc. than any model of concurrency.

~~~
mpweiher
Channels are lifted from CSP, but PAR is obviously missing.

~~~
zbobet2012
par is "select" effectively. While that isn't 100% the case, it is true.

~~~
mpweiher
Huh? I thought "select" was "ALT"??

~~~
kolpa
'go' is PAR with different syntax (Everything after the 'go' line is
implicitly one branch of the PAR)

'select' is ALT

~~~
mpweiher
Hmmm...as far as I understood the FA, the point is exactly that 'go' is
_unlike_ PAR in important respects.

'go' is more like a goto, the new goroutine is spawned, without any scoping.
PAR opens a scope where all the contained routines are executed in parallel,
but all of these must have terminated before the statement after the PAR is
executed.

?

------
lclarkmichalek
This is usually why I end up using
[https://godoc.org/golang.org/x/sync/errgroup](https://godoc.org/golang.org/x/sync/errgroup)
instead of straight go statements, as it addresses some of the cancellation
and error propogation issues. When I think of my use of naked go statements,
it's usually for periodic tasks; having something similarly structured for
them would be a clear win to me (though potentially the impact is less
significant, as it's less painful to write a well formed periodic task using
the go statement and context).

~~~
lobster_johnson
ErrGroup is nice, but it was created before contexts existed, and doesn't have
support for cancellation. I have a bounded worker pool executor that handles
cancellation that I'm currently extracting from a private project; shout out
if interested.

~~~
lclarkmichalek
The errgroup I linked has a single constructor `WithContext(ctx.Context)
(*Group, context.Context)`. I think you might be thinking of the stdlib's
sync.ErrGroup :)

~~~
lobster_johnson
Oops, I didn't realize that. Thanks. Mine implementation supports a bounded
(max concurrency) mode, though!

------
Animats
This addresses the wrong problem. The real issue is control over data shared
between threads, not control flow.

C/POSIX type threads have no language support for indicating what data is
shared and which locks protect which data. That's a common cause of trouble.
The big question in shared memory concurrency is "who locks what". Most of the
bugs in concurrent programs come from ambiguities over that question.

Early attempts to deal with this at the language level included Modula's
"monitors", the "rendezvous" in Ada, and Java "synchronized" classes. These
all bound the data and its lock together. Rust's locking system does this, and
is probably the most successful one so far. (Yes, the functional crowd has
their own approaches.)

Go talked a lot about controlling shared memory use. The trouble with
goroutines, as Go programmers found out the hard way, was that the "share by
communicating, not by sharing" line was bogus. Even the original examples had
shared data. But the language didn't provide much support for controlling that
sharing.

Python is basically at the C level of sharing control over data, except that
the Global Interpreter Lock keeps the low-level data structures from breaking.
This prevents Python programs from doing much with multi-core CPUs. Since this
is just another thread library for Python, it has the same limitations.

Real concurrency in Python with disjoint data, and without launching a heavy-
weight subprocess, would be a big win. But this isn't it.

~~~
rumcajz
Almost all attempts at CSP-style programming in the end resorted to sharing
data to get a little bit better performance. I wonder whether we shouldn't
have used a bit of speedup that Moore's law gave us to cover that cost and be
done with all the shared state headaches.

~~~
kllrnohj
Except shared data doesn't give you a little bit better performance, it gives
you _massively_ better performance. Or, in some cases, it's the only way to
get usable performance at all.

Now what you could do is break objects down into annotated types. Consider
immutable vs. mutable in combination with thread-unsafe, thread-compatible,
and thread-safe. Immutable data that's not thread-unsafe you can share freely
across threads, all is well. L2/L3 caches are happy. Mutable that's thread-
safe can similarly be shared at will. Then you can force that thread-
compatible objects be wrapped & accessed only from a Mutex or transfered
between threads as part of a move operation.

Rust gives you the tools to do all of this, and indeed does some of it, but as
part of the steep learning curve of the ownership model.

~~~
Animats
I proposed something like that for Python in 2010.[1] Immutable objects could
be shared. Mutable objects had to either be unshared, or a subclass of an
object that enforced synchronization.

Python's Little Tin God didn't like it. Mostly because I proposed to freeze
the code of the program once the second thread started. That takes away much
of the dynamism he insists on.

Might be worth looking at again. The separation of data into immutable,
unshared, or synchronized is mainstream now.

[1]
[http://animats.com/papers/languages/pythonconcurrency.html](http://animats.com/papers/languages/pythonconcurrency.html)

~~~
kllrnohj
Python seems like an odd place to try and shove this into. Both due to its
heavy object mutability & dynamic nature in combination with the GIL making
heavy threading of python code a waste of everyone's time anyway.

There aren't many languages with the concept of ownership or moving at all,
and trying to retro-fit that is probably going to be not a good experience for
anyone involved.

Rust is largely there, in yet another thing it does well, but if you don't
want that something like C++ would be probably a better place to try it. There
you at least have move & ownership as a language concept already.

~~~
Animats
When I wrote that, Rust didn't exist, C++ didn't use move semantics much, and
Python was more important than it is now.

------
m0meni
I thought the title was kinda clickbaity, but it turned out to be a great
article. Also the comparison to goto really effectively conveyed the point he
was trying to make. I have two questions though:

* Does anything else like this currently exist (other than the Trio library he mentions), which shows that it's a superior paradigm in practice?

* What are the cons to this approach? Why not do it?

~~~
jeremiep
I don't think its a superior paradigm, just a different one.

I only see his nursery as being useful when you really want your async tasks
to complete before the function in which they were dispatched returns. That's
far from covering every use case of concurrency!

A lot of the value of concurrency is in background operations. These simply
can't be tied to the duration of a function call on the dispatch thread. Doing
so will literally kill any advantages of concurrency in the first place, might
as well just block directly. (This is especially true of apps modelled as an
update loop, you definitely don't want to block that loop.)

I can think of very few places where I'd actually want a nursery, and even in
those cases I'd rather use the promises or fork&join already available.

~~~
dingo_bat
> A lot of the value of concurrency is in background operations. These simply
> can't be tied to the duration of a function call on the dispatch thread.

If your background operation has those characteristics, maybe it is better to
spawn a new process for it. Why a thread?

~~~
jeremiep
A process has much higher overhead (both in time and space), more complex
communications and is not always possible in the first place. It also adds
complexity to the build pipeline. These quickly adds up to being not worth it
over a task or thread.

You wouldn't spawn a process for the
rendering/input/physics/network/job/loader threads of a game engine for one.
You can't spawn processes from a web app either.

Threads are actually pretty darn simple when you have either immutable data or
uniqueness constraints. Deterministic parallelism is also very powerful while
preventing all sorts of nasty bugs.

~~~
dingo_bat
> A process has much higher overhead (both in time and space), more complex
> communications and is not always possible in the first place.

This can change if OSes start optimizing for the proposed convention.

> It also adds complexity to the build pipeline.

At least in C/C++ it doesn't. But again, this is also easily solvable if we
want to.

> You wouldn't spawn a process for the
> rendering/input/physics/network/job/loader threads of a game engine for one.

Why not? In my mind, if you need 2-way communication between 2 threads, they
should be siblings. If they need one way communication, there should be a
parent-child structure. If they need no/minimal communication, they are better
off as processes. I don't know which categories each of the threads you named
fall into. I realize that this approach will require extensive redesign of
existing software, very much like the elimination of goto required.

> You can't spawn processes from a web app either.

No reason for it to stay that way.

> Threads are actually pretty darn simple when you have either immutable data
> or uniqueness constraints. Deterministic parallelism is also very powerful
> while preventing all sorts of nasty bugs.

I won't pretend to know all those words :P I just think the article's proposal
has some merit and we should consider it. >

~~~
jeremiep
> This can change if OSes start optimizing for the proposed convention.

What conventions? Its not realistic to assume the world will change to fit
your views of software :p

> At least in C/C++ it doesn't.

Sure does; it adds more build targets, gets you to maintain shared code across
executables, and plan deployment for multiple executables instead of one.
Thats all before even coding the support for that.

> Why not?

Because these depend on shared memory and ownership transfer for performance;
you'll drastically drop performance just for the sake of isolation.

> No reason for it to stay that way.

I will literally stop using the web if pages can spawn processes :)

> I won't pretend to know all those words :P

Hehe, basically immutability makes it so nobody can mutate data, thus making
it safe to be shared across threads. Uniqueness will transfer ownership such
that only one thread has references to a mutable piece of memory at any time.
Deterministic parallelism means its impossible to have race conditions or
deadlocks.

> I just think the article's proposal has some merit and we should consider
> it.

Agreed, I'm still having a hard time seeing it however :p

------
coldtea
It's amazing how many people managed to skim through the post, and hammer on
their own preconceptions and facile counter-arguments, for things that are all
addressed in the argument.

And that's for a very well written post, that tries to address all common
issues.

And yet, people manage to get it wrong, or write facile responses like "re-
implementing the fork/join".

Not to mention missing the whole nuance of what the author is talking about,
which is not about novelty of a feature, but about what it allows us (and even
more so, what it constraints us).

It's like as if people being shown for loops and structured programming in the
60s responded with "this proposal just reinvents gotos". Or worse, that "this
is more restrictive that gotos".

Yes, the author knows about the Erlang's model. He writes about it in the
post, and about how you can use his proposal to do something similar.

Yes, the author knows about Rust's model. In fact Graydon Hoare, the creator
of Rust (now working at Apple on Swift), has read the post's initial draft and
gave his comments to the author.

~~~
twic
If the author knows about Rust, it's a little odd that he didn't mention
scoped-threadpool, which is pretty close to this idea:

[http://kimundi.github.io/scoped-threadpool-
rs/scoped_threadp...](http://kimundi.github.io/scoped-threadpool-
rs/scoped_threadpool/struct.Pool.html)

EDIT: He compares to Rust in a comment on the Reddit thread:

[https://www.reddit.com/r/programming/comments/8es8x3/notes_o...](https://www.reddit.com/r/programming/comments/8es8x3/notes_on_structured_concurrency_or_go_statement/dxxucgi/?st=jggijgyw&sh=3a0dced9)

As an aside, HN users tend to sneer at Reddit, but this is another case where
the discussion on Reddit is better than the one here.

------
leshow
> As a result, every mainstream concurrency framework I know of simply gives
> up. If an error occurs in a background task, and you don't handle it
> manually, then the runtime just... drops it on the floor and crosses its
> fingers that it wasn't too important.

You ought to look into Erlang and Elixir on the BEAM vm/runtime. It's arguably
the best example of this kind of concurrency (greenthreading, async) done
properly with regards to error handling.

I don't write Elixir or Erlang, but I believe this process is managed by the
supervisor. You can select various behaviours for when a process crashes or
errors out[1]. For instance, you can have a process simply restart after it
crashes. Combined with a fail-fast mentality, this produces remarkably fault
tolerant and long lived applications.

[1]:
[http://erlang.org/doc/design_principles/sup_princ.html](http://erlang.org/doc/design_principles/sup_princ.html)

~~~
jlouis
Somewhat agree.

Supervision trees (or sync.WaitGroup in Go) are good tools for achieving the
same end result. But it isn't as semantically protected as the article states.
In Trio, the property is given by the programs scope.

However, if process creation/termination follows the scoping rules of the
program, I have a hunch you run into situations where certain things are not
only hard, but outright impossible to express.

Now, the author gambles that this is a good thing and we will eventually find
good structural solutions to all the problems. I, on the other hand, is a bit
more pessimistic because it has been tried before and found to be lacking.

I wonder how Trio handles error propagation.

~~~
elcritch
Not sure what you mean by semantically protected? OTP supervisor trees enforce
the constraints that are specified in the supervisor’s start function. It’s
semantics are defined by the OTP library and function scopes. As you mention
there are times to break out of the strict (supervisor) pattern if
needed/wanted and start processes manually. Otherwise the default behaviors
provide a nice set of limited behaviors that have proved useful across a broad
spectrum of situations.

Really what TFA’s discussing seems much more akin to OTP than raw Erlang. Or
more specifically a subset of it. Occasionally I wish there were a few more
OTP supervisor behaviors but nothing that’s a show stopper. Not familiar with
Go’s WaitGroup but I haven’t seen it used much in code I’ve read.

------
shadowmint
This is very very long to explain a very simple pair of ideas:

1) You should be able to declare scoped blocks that mandate execution of all
tasks started in that block ends when the scope ends.

2) This is fundamentally superior to all other forms of concurrency.

I get it; this is basically what async/await gets you, but conceptually you
can spawn parallel tasks inside an awaited block, and know, absolutely that
all of those tasks are resolved when the await resolves.

(this is distinct from a normal awaited block which will execute tasks inside
it sequentially, awaiting each one in turn).

...seems like an interesting (and novel) idea to me, but I flat our reject (2)
as ridiculous.

Parallel programming _is hard_ , but the approach from rust, to give you
formal verification, instead of arbitrarily throwing away useful tools seems
much more realistic to me.

~~~
appleflaxen
The article makes a strong case. Even if rust _allows_ formal verification,
not all programmers use it, and if you use their library, you don't know if
it's been verified.

Take it back to the goto analogy: can you formally verify goto? Yes. Does that
mean it's good to include as a language primitive? No.

You are basically taking an entire argument, ignoring its merits, and saying
"but you can do it another way". You are exactly right, but you haven't
rebutted the fact that structured concurrency is philosophically superior.

I love rust and the community; they will get to the truth of this argument
eventually. But I suspect the truth is that "njs was right", and I hope the
it's sooner rather than later.

~~~
wilun
It's not only that Rust allows formal verification, it's that it does it by
default, writing unsafe Rust code for no serious reason is fundamentally
frowned upon, and even then there is a serious effort to bring some tooling to
bring more confidence even on "unsafe" Rust code.

> But I suspect the truth is that "njs was right", and I hope the it's sooner
> rather than later.

I'm not sure. The problem can and has been solved without the manually "pass
the nursery object around" ""escape hatch"" for the _general case_ of threads
(because no: having the execution of spawning functions delayed by the
lifetime of thread is arguably reasonable is _some_ cases, but certainly not
the _general case_ of what threads are useful and used for)

It is still useful for tons of existing languages as a pattern anyway, but
only _if_ the use cases are suitable.

But the author is focused on a narrow use case of threads (and a narrow subset
of the problems they introduce), and present their solution as a general truth
and new fundamental control structure of computing, independent of already
existing, in production, and arguably better solutions; and independent of
analyzing the new problems their silver bullet introduces.

~~~
iainmerrick
“Formal verification” for Rust code -- does that actually exist yet, or do
people just hope/assume that the language will be proven consistent and that
awesome theorem-checking tools will emerge eventually?

I agree that Rust has a great model that seems to lead to very solid code, but
“formal verification” is a high bar to clear.

~~~
mikeurbach
Some people are actively working on formal verification for Rust. The RustBelt
project is the first that jumps to mind. A paper and some discussion here:
[https://news.ycombinator.com/item?id=16302530](https://news.ycombinator.com/item?id=16302530)

------
jaffee
I LOVE Go's concurrency model, but this article has won me over (pending some
experimentation anyway).

If you just skimmed, this is actually worth a careful read. The parallels
between "go" and "goto" are explained very clearly, and you get some awesome
Dijkstra quotes to boot!

~~~
acjohnson55
I think people are missing the merits of what the author said in their
defensiveness of their existing approaches, which is a bit sad.

~~~
tomtheelder
The article had a whole section on how any challenges to existing paradigms
and tools will be met with fierce opposition. The comments here are an
excellent illustration!

------
pnathan
Article persuasive _prima facie_ and arguments plausible. I've had to reinvent
a structured method of managing threads a number of times, unfortunately.

Author is a PhD student, which bodes well for not reinventing wheels dumbly.
Therefore, I look forward to the lit review of other concurrency & parallelism
work through the last 40 years, which this writeup notably lacks (author
mentions his stack of papers to review).

 _Your ideas are intriguing to me and I wish to subscribe to your newsletter._

ed: author has phd, not is a student.

~~~
eridius
> _Author is a PhD student, which bodes well for not reinventing wheels
> dumbly._

Except that's exactly what the author did. They just reinvented scoped
threadpools.

~~~
coldtea
In the sense that a car is a reinvention of a horse buggy.

~~~
eridius
In what way? Nurseries sound like they're exactly the same thing as a Rust
scoped threadpool with a 'static lifetime. Not some sort of super
modernization like you're suggesting.

------
bgorman
I have yet to see any of these problems in core.async (Clojure's version of
goroutines). core.async has enabled fantastic programming abstractions like
async pipelines using transducers and "socket select" type programming.
Perhaps functional programming is the solution to solve concurrency issues.

~~~
jeremiep
Clojure has HUGE advantages over Go in the first place, that helps immensely
in making concurrency sane and safe. Most of the goodness in Clojure is
emergent of its design, very few languages have that property.

The simple fact that Clojure can implement goroutines as a library shows how
flexible the language is. Then there's immutability and a strong focus on
simplicity among others.

------
jnwatson
What timing!

My project is currently struggling with how to migrate to Python async. The
biggest challenge is the place where async and sync interface.

Just the other day, my colleague was wondering out loud about the possibility
of using a context manager to constrain the scope of async. This is it. This
is exactly what we were looking for.

------
acjohnson55
So, my initial thought when reading was "yeah, it's async/await". But it's
subtly different than that though.

You're free to spawn parallel tasks in async/await -- you just use Promise.all
or Future.sequence, or whatever your language provides to compose them into a
larger awaitable.

Nurseries seem to go a step beyond this by reifying the scheduling scope as
the eponymous nursery object. This means that you have a new choice when the
continuation of your async task happens: a nursery pass in from some ancestor
of the call tree. My gut says that this offers similar power as problematic
fire-and-forget async tasks, but takes away the ability to truly forget about
them.

My guess is that, in practice, you end up with some root level nursery in your
call stack to account for this. But account for it you must! And while the
overwhelming sentiment in these comments is pretty dismissive, I'd caution
against downplaying the significance of this. It's basically like checked
exceptions or monadic error handling.

I also think about how this maps to task or IO monad models of concurrency. It
seems like there's an inversion of control. Rather than returning the
reification of a task to be scheduled later, the task takes the reification of
a runtime, upon which to schedule itself. I'm not sure what the ramifications
of this are. Maybe it would help with the virality of async return values [1],
but at the cost of the virality of nursery arguments.

Lastly, one thing this article nails is the power of being able to reason
about continuation of control flow. Whether or not nurseries have merit as a
novel construct, this article still has a lot of educational use by making
this argument very clearly. Even if the author is wrong about nurseries being
"the best", it sets a compelling standard that all control mechanisms--async
or not--should have to explain themselves against.

I do have a couple questions:

\- In the real world, would library APIs begin to get clogged with the need
for a nursery on which to run an async task? Think async logging or analytics
libraries.

\- Would usages similar to long-running tasks that receive async messages be
compatible? I'm thinking of usages of the actor model or channel model that
implement dynamic work queues.

\- Does this increase the hazard presented by non-halting async tasks?

[1] [http://journal.stuffwithstuff.com/2015/02/01/what-color-
is-y...](http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-
function/)

------
panic
This other post from the same blog gives some more concrete background on why
this sort of structured concurrency is a good idea:
[https://vorpus.org/blog/some-thoughts-on-asynchronous-api-
de...](https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-
post-asyncawait-world/)

------
shalabhc
"our call stack has become a tree"

This is a really useful property to have and reason about.

Instead of several independent coroutines with arbitrarily overlapping
lifetimes, we can now think of all coroutines as organized in a _single
hierarchy_ with properly nested lifetimes.

The function call stack becomes a call tree - each branch is a concurrent
execution.

------
catnaroek
Lately, I am of the idea that the real problem with how we do concurrency is
that we have yet to figure out a way to do it without first-class procedures.
When we spawn a thread, even in a low language such as C, we use something to
the effect of:

    
    
        launch_thread(function, perhaps, some, initial, data);
    

The trouble with this approach to concurrency is twofold:

(0) It forces a hierarchical structure where one continuation of the branching
point is deemed the “parent” and the others are deemed the “children”. In
particular, if the forking procedure was called by another, only the “parent”
continuation may return to the caller. This is unnatural and unnecessarily
limiting. Even if you have valid reasons to guarantee that only one
continuation will yield control back to the caller (e.g., to enforce linear
usage of the caller's resources), the responsibility to yield back to the
caller is in itself as a resource like any other, whose usage can be
“negotiated” between the continuations.

(1) It brings the complication of first-class procedures when it is often not
needed. From a low-level, operational point of view, all you need is the
ability to jump to two (or more) places at once, i.e., a multigoto. There is
no reason to require each continuation to have a separate lexical scope,
which, in my example above, one has to work around by passing “perhaps some
local data” to `launch_Thread`. There is also no reason to make “children”
continuations first-class objects. If you need to pass around the procedure
used to launch a thread between very remote parts of your program, chances are
your program's design is completely broken anyway. These things distract the
programmer from the central problem in concurrent programming, namely, how to
coordinate resource usage by continuations.

------
_Codemonkeyism
The main culprit to me in Golang seems to be channels, not goroutines. If your
workflow essentially is defined by a mesh of channels and goroutines, it's
hard to reason or understand.

I have no direct practical knowledge of Golang, but working on a large
application that used BlockingQueue for concurrent communication and one which
extensively used services buses for communication - both were hard to
understand and reason about flow.

After some years with Scala Futures I'd say they work well and reason well.
They can be seen as normal function calls returning Future instead of another
'container'.

They reflect the black box mentioned in the article, with one way in and one
way out (e.g. when a method returns Future[_]).

The point about error handling: We use Option,Seq.empty on read error
handling, Validation on create/write and Either on side effects (like sending
mail).

(yes, they are still leaky abstractions e.g. when debugging, but work fine
most of the time)

~~~
acjohnson55
I think the article's point is that with Future's you can still pretty easily
invoke a Future-returning function and forget to return its value, ending up
with what you might call an orphan continuation.

~~~
_Codemonkeyism
If the future has a side effect I'm not concerned with, like sending a mail, I
can't see the problem?

~~~
acjohnson55
The problem is much like the author said -- it's easy to have errors disappear
into the ether in a way that is much less likely in synchronous logic. Also,
if those side-effects matter, it's easy to make faulty assumptions about time
ordering.

The most obvious situation to me is in the way asynchrony exists in front-end
programming and how this affects testability. If you can't actually know when
a process (like an animation) ends, you can't accurately test.

In general, my experience has been that reification of abstract things often
presents benefits in the long run. Reification of functions admits a whole
host of techniques. Reification of classes facilitates metaprogramming.
Reification of in-flight processes as promises helps with being able to
compose and abstract over them. Nurseries seem like reficiation of an finite
execution context.

~~~
_Codemonkeyism
I'm not following, how could an error with Future[Either[A,B]] disappear
compared to synchronous logic of Either[A,B]? Our code base is the same for
sync and async logic and error handling.

One thing that doesn't work is Anders Hejlsbergs method of letting unchecked
exceptions bubble up, but exceptions haven't been a good idea for business
code anyway.

------
_bxg1
Interesting ideas, and well-worded.

You could achieve something similar in JavaScript with Promise.all() and
await:

    
    
      await Promise.all([
        asyncFunc1(),
        asyncFunc2(),
        asyncFunc3()
      ])
    
    

Of course, that's not language-level and the point seemed to focus more on
eliminating traditional branching than just adding another way to do it.

~~~
Osiris
That's exactly what I was thinking. Some of us are probably are using this
type of control flow. For me a common scenario is something like

    
    
        const promises = files.map(readFileAsync); // "nursery"
        const fileContents = await Promise.all(promises); // "with"
    

I generally agree with his premise that it sucks having to figure out if a
function is concurrent or not; i.e., does it return a value or a
Promise/Future. I'm not sure if his solution solves that particular issue
though, unless it's handled automatically in his "nursery.start_soon"
function.

------
woah
Worst title ever. I never complain about this stuff, but can someone please
change it? You think it’s going to be some analysis about Go, but instead it’s
someone pushing their library.

~~~
algorithmsRcool
I think this article is a lot more than just "someone pushing their library"

It is well organized, written and specific about it's claims.

------
cousin_it
Didn't expect to say this, but the article is completely right! This is
obviously the right way to write concurrent programs. Kudos for writing this.

One question though. The first part of the article says that "onclick"
handlers should be replaced with nurseries as well. But I don't see how. Can
someone explain?

~~~
jerf
I suspect that if you were to try to do this in real code, something like an
onclick handler would have to be run in a nursery scoped at the page level, by
the code running the page. An onclick handler on its own accord can't run
itself in the correct nursery. It's like the network handling case there,
where the nursery's lifetime is either unbounded, or tied to the lifetime of
the OS process, depending on how you want to look at it. Functions don't
always return within a program, or at least, don't always return excepting
maybe a last cleanup as the program terminates.

~~~
marshray
Maybe a subtle difference here is that a page doesn't get to block on anything
before it gets closed.

So how is creating a nursery that matches the lifetime and visibility of the
page different from not using a nursery at all?

~~~
jerf
"Maybe a subtle difference here is that a page doesn't get to block on
anything before it gets closed."

In real browsers, the UI act of closing a tab and the completion of cleaning
up all of its resources are already clearly separated. I can see that when I
terminate a browser with a lot of tabs and the main window has entirely closed
while the browser runs at 100% CPU for quite a few more seconds. Plus a
browser can probably hard-kill running Javascript code with some reasonable
effectiveness. (Not sure. Asynchronous exceptions are very, very hard.)

So I don't think that's a disqualifier.

"So how is creating a nursery that matches the lifetime and visibility of the
page different from not using a nursery at all?"

Well, as I've said in one of the reddit conversations, bear in mind the entire
purpose here is not to enable something that was previously impossible, but to
_constrain_ us from using primitives in their most powerful form. The big
thing is that if you had a nursery-based system, and the page had a nursery
associated with it, and the page's render routine returned, you'd _know_ that
all the page threads must necessarily be terminated. You can't know that with
the same confidence now, because the programming style does not permit that
level of confidence by construction.

Browsers are a bit of a pathological case on a lot of levels, though, and
probably not a great mental example, because you can assume a high degree of
competence, concern, and skill in the people programming the browser, and they
do things like use static analysis all the time and even in the "worst" cases,
develop entire new programming languages to write browsers in. So you are
probably justified in saying "But jerf, I'm pretty confident the browsers are
already cleaning up their stuff without this stuff." The real question is, how
does this look for Joe Programmer and his ability to work in the domain of
multithreaded programming, which is well known and widely acknowledged to be
very difficult, by constraining what mistakes he can make?

One of the other angles you can look at this with is, if I have two pieces of
correct code and I compose them together, are they still correct? The nursery
system says that with my nursery, I can call other code that uses them, and
the composition attained over that function call is also correct and does not
leak resources. We do not get that guarantee with some other primitives. We do
with others. There's a lot of experimentation still going on in this field.
I'm not saying this approach is guaranteed to be correct, but it's one of the
more plausible claims to being a primitive as basic as "if" is relative to
"goto". Compare with Software Transactional Memory, which as nice as it may
be, is wildly more complicated than "if" no matter how you slice it.

------
ptero
I was underwhelmed by reading this (maybe due to a clickbaity title and IMO
sketchy extension of badness from goto to the go statement).

That said, the article has good technical content. It proposed a new
concurrency library with interesting properties. Concurrency comes with
additional cost. The library proposes a paradigm to minimize certain costs and
should provide punchy examples of how things can be done simply and
efficiently with it.

But instead it is picking shallow fights with the go statement (does the
author know about the "sync" package and WaitGroup)? Overall I found the
advocacy section _WAY_ too long. Use most of that real estate to show goodness
of your library, not on trying to punch holes in the competitors. My 2c.

------
liveoneggs
[http://rvirding.blogspot.com/2008/01/virdings-first-rule-
of-...](http://rvirding.blogspot.com/2008/01/virdings-first-rule-of-
programming.html)

------
amluto
I’m wondering if some promise/future systems already provide a similar
guarantee. The main useful property of the nursery system is that a function’s
signature indicates whether that function leaves a background task running. If
you can guarantee that promises/futures are dropped if they go out of scope,
then you get a similar guarantee. Similarly, if you have a system where
promises don’t actual run their background parts unless someone is waiting for
them, then there are no leaky background tasks.

------
jey
I'm really growing fond of the async/await abstraction. How do I get this in
more C-like languages like Go, Rust, or C++? (I have a bunch of C++ code that
I want to call via C ABIs.)

I'm intrigued by libdill but it's mysterious enough that I'm scared to include
it in my project -- I don't want to risk getting sidetracked by having to
debug my concurrency primitives.

~~~
tmandry
Rust is working on exactly this, targeting stabilization later this year:
[http://aturon.github.io/2018/04/24/async-
borrowing/](http://aturon.github.io/2018/04/24/async-borrowing/)

~~~
steveklabnik
The RFC is in its final comment period: [https://github.com/rust-
lang/rfcs/pull/2394#issuecomment-383...](https://github.com/rust-
lang/rfcs/pull/2394#issuecomment-383773009)

Very excited!

------
benjohnbarnes
I found this pretty fascinating and I'm looking forward to hearing the
theoreticians chew it over.

A question I had was with the API that's been chosen. The `nursery` is chosen
as the reified object, and a function `start_soon` is exposed on it. Perhaps
in other parts of the library there are other methods exposed on `nursery`? If
not, in some languages it seems like the `start_soon` method itself would make
more sense as the thing to expose. In use, it might do like this:

    
    
        ...
        nusery {
          (go) in
          go { this_runs_concurrently_in_the_nursery }
          go { this_also_runs_concurrently_in_nursery }
          // make a regular function call passing it the nursery's `go`
          some_func(go)
        }
        ...
    

And elsewhere:

    
    
        ...
        func some_func(go) {
          do_something()
          go {
            nursery {
              // This nursery is within the outer one.
              (go2) in
              go2 { do_stuff }
              go2 { do_more_stuff }
            }
          }
        }
        ...

------
Osiris
> whenever you call a function, it might or might not spawn some background
> task. The function seemed to return, but is it still running in the
> background? There's no way to know without reading all its source code,
> transitively. When will it finish? Hard to say.

This reminds of me "colored functions" (red vs blue) where it becomes
imperative to know if a function you are calling returns a value or a
Future/Promise.

Some languages allow annotating a function to indicate as such so the IDE can
help. His particular solution he presents actually doesn't address this
question: Is your function sync or async? You still have to know when calling
a function if it's async and needs to be in a nursery or not.

Should a programming language abstract away whether a function is async or
not? async/await is a step forward (C#/JS) but it still requires knowing if
the child function is async or not.

------
ufmace
I found it an interesting idea and writeup. It'd be good to see what could be
done in a language that implemented this - concurrency only allowed in the
context of nurseries. On the downside, though, I think there's a lot of
concurrency patterns that could only be implemented by creating a nursery near
the top level of the application and passing it around all over the place,
thus getting you pretty much right back where you started.

Want a web app that sets up some long-running thing to run in the background
while the request returns quickly? Well then you're going to need a nursery
above the level of the request which is still available to every request. I
don't see what that gives you above conventional threading. Oh, and you'd also
need to implement your own runner in a thread to have a task failure not bring
down the whole application.

~~~
yxhuvud
> I think there's a lot of concurrency patterns that could only be implemented
> by creating a nursery near the top level

Yes, the same thing is true of goto and other control flow patterns. Patterns
change to match the tools available.

------
mamcx
Is incredible how easy is to miss the point of this.

Let me try with something else.

Imagine you have try/catch/finally BUT NOT AS CONTROL FLOW CONSTRUCS but "just
api calls".

So, you language need to be used like:

    
    
        foo()
        exceptions.try{
        	bar()
        	this.catch{
        	
        	}
        }
    

It means, you need to remember to ALWAYS REMEBER to "close" the start of the
call.

Imagine how bad this could be. If only "try/catch" was as with "IF/ELSE/ENDIF"
so you not do something stupid like:

    
    
        foo()
        exceptions.catch{
        	what?()
        }
        bar()

------
earenndil
His note on resource cleanup doesn't really make sense -- or, rather, it only
makes sense if you're using python's solutions for it. What about something
like, say, c++ where you have a smart pointer that keeps track of the number
of references to it, to store your file pointer? Then when you pass that to
your concurrent function, it won't get cleaned up, until that other thread
finishes, then that reference to it will be lost and it'll clean itself up.

------
atrn
Coming from an occam/transputer background (in the 1980s) I felt goroutines
were too low level. Luckily its trivial to add a PAR-like construct via
sync.WaitGroup (e.g. github.com/atrn/par). That said Go's channels are far
easier to work with - buffering and multi-producer/-consumers being very
common needs which, in occam, you implement yourself. The lack of guards in
Go's ALT (select) is a shame and the nil channel hack is just that, a hack.

------
zzzcpan
I know people already said it, but causality, resource cleanup and error
handling are all solved with Actor model in a more general, more flexible and
more reliable way than nurseries.

If you think reasoning about concurrency is hard, try testing and modelling
it, especially for something distributed. This is where naive ideas about
concurrency should start to fail and a need in solid foundation arise.

------
egnehots
There are other approaches. State machines, reactors are often used in low
level layers and the Actor model for some high level design.

~~~
mindB
When you add in queues for passing things back and forth between the async
functions, I think this boils down to an Actor model. The nursery would be
equivalent to supervisors in Erlang.

------
titzer
The author might want to take a look at Habanero Java, which has all of this
and more.

[https://wiki.rice.edu/confluence/display/HABANERO/Habanero-J...](https://wiki.rice.edu/confluence/display/HABANERO/Habanero-
Java)

------
glorioustao
go statement is just another form of explicit process creation, fork/join
pattern. What the author suggested is just similar to cobegin/coend --
implicit process creation.

cobegin/coend are limited to properly nested graphs, however fork/join can
express arbitrary functional parallelism (any process flow graph) [1]

Yes, for graceful error handling it needs to form some sort of process tree or
ATC(Asynchronous transfer of control), which is implemented in Erlang/OTP and
ada programming.

[1]
[http://www.ics.uci.edu/~dillenco/compsci143a/notes/ch02.pdf](http://www.ics.uci.edu/~dillenco/compsci143a/notes/ch02.pdf)

------
pmarreck
I don't like the term "nursery" (maybe "highway" or "complex" or... something
else) but this seems to be a good design change, unless I'm missing something

~~~
Osiris
I believe it's used because it keeps track of "children", or child functions
spawned by the current function. Without knowing that background however, it's
not immediately obviously to someone that hasn't heard the term what it means.

As others have mentioned, reusing the "await" keyword could cover a lot of
these nursery scenarios.

------
xfer
How is this different than calling join(), after spawn()?

~~~
jerf
How is if different that just using goto to go to either the true or false
clause? It isn't about what you can do, it's about what you _can 't_ do if you
use this mechanism (this restricts where you can call spawn and join) and the
guarantees you can then build on top of that.

~~~
xfer
build what on top of it? If i want a blocking thread i would just call a
function.. something something goto something isn't a valid argument that
applies to everything. The other way to propagate errors up is chaining
promises.

~~~
dingo_bat
> If i want a blocking thread i would just call a function

That is not what this is though.

------
themihai
Well they lost me at the callbacks... if go statements are harmful and
callbacks are not then I rather prefer the harmful way.

~~~
vishvananda
You might want to read further. He didn't claim that callbacks are not
harmful. In fact he suggested that they suffer from the same problems and he
is using "go statements" to encompass all of the forms of concurrency
handling.

------
imauld
How did someone manage to write a post that long discussing Go and it's
concurrency model and not once mention channels?

------
bitL
That's pretty cool! Thanks for enriching my day and good luck with your
framework!

------
Fellshard
A point I believe the author has missed: Goroutines aren't simply forked
functions, but are abstracted as independent, autonomous processes. Now, that
doesn't mean 'go' is a sufficient tool to reason about goroutines, but this
may also not be as useful a solution as he touts it to be.

~~~
tomtheelder
I don't think that's really relevant to his point. His argument is about
control flow, rather than capabilities. The library he has built is designed
to provide any functionality that didn't previously exist, but rather to make
code that accomplishes the same tasks as before easier to reason about and
somewhat safer to write.

An implementation of this pattern could handle the spawning of functions
however it wants and the language supports (as independent processes, threads,
with an event loop, etc.).

------
ychen306
Hmm.. Looks like the author is reinventing fork-join style parallelism...

~~~
coldtea
Looks like people skim articles and apply anything they already know and
recognize to explicitly more evolved ideas...

------
gafferongames
This is absurd. The only thing "go" has in common with "goto" is the first two
letters.

------
dingo_bat
I think this has a lot of merit. And the analogy to goto is very apt and deep.
I suspect that 20 years later all our concurrency interfaces may well look
like this.

However, the momentum of today's programming community may be too great to
surmount. When goto was criticized, there were fewer people to convince to
give up on it. Now, there are orders of magnitude more devs. And all of them
are comfortable in the current way of doing things.

