What.<p>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.<p>It then uses an exception-like model for error propagation to "solve" error handling (which is fairly easy to handle with a channel).<p>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?).<p>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 <i>branch</i> 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.<p>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 <i>callbacks</i>, something which have nothing to do with concurrency <i>at all</i>. Very odd.
The author appears to have reinvented Communicating Sequential Processes (CSP).<p><a href="http://www.usingcsp.com/cspbook.pdf" rel="nofollow">http://www.usingcsp.com/cspbook.pdf</a><p>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.<p>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.<p>eg.<p><pre><code> PAR
longCall1
longCall2
longCall3
SEQ
reduceAllThreeResults
doSomethingWithTheReducedResult
PAR
nextParallelOp1
nextParallelOp2
</code></pre>
etc.
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.<p>And that's for a very well written post, that tries to address all common issues.<p>And yet, people manage to get it wrong, or write facile responses like "re-implementing the fork/join".<p>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).<p>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".<p>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.<p>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.
This addresses the wrong problem. The real issue is control over data shared between threads, not control flow.<p>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.<p>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.)<p>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.<p>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.<p>Real concurrency in Python with disjoint data, and without launching a heavy-weight subprocess, would be a big win. But this isn't it.
This is usually why I end up using <a href="https://godoc.org/golang.org/x/sync/errgroup" rel="nofollow">https://godoc.org/golang.org/x/sync/errgroup</a> 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).
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:<p>* Does anything else like this currently exist (other than the Trio library he mentions), which shows that it's a superior paradigm in practice?<p>* What are the cons to this approach? Why not do it?
> 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.<p>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.<p>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.<p>[1]: <a href="http://erlang.org/doc/design_principles/sup_princ.html" rel="nofollow">http://erlang.org/doc/design_principles/sup_princ.html</a>
This is very very long to explain a very simple pair of ideas:<p>1) You should be able to declare scoped blocks that mandate execution of all tasks started in that block ends when the scope ends.<p>2) This is fundamentally superior to all other forms of concurrency.<p>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.<p>(this is distinct from a normal awaited block which will execute tasks inside it sequentially, awaiting each one in turn).<p>...seems like an interesting (and novel) idea to me, but I flat our reject (2) as ridiculous.<p>Parallel programming <i>is hard</i>, but the approach from rust, to give you formal verification, instead of arbitrarily throwing away useful tools seems much more realistic to me.
I LOVE Go's concurrency model, but this article has won me over (pending some experimentation anyway).<p>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!
Article persuasive <i>prima facie</i> and arguments plausible. I've had to reinvent a structured method of managing threads a number of times, unfortunately.<p>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).<p><i>Your ideas are intriguing to me and I wish to subscribe to your newsletter.</i><p>ed: author has phd, not is a student.
What timing!<p>My project is currently struggling with how to migrate to Python async. The biggest challenge is the place where async and sync interface.<p>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.
"our call stack has become a tree"<p>This is a really useful property to have and reason about.<p>Instead of several independent coroutines with arbitrarily overlapping lifetimes, we can now think of all coroutines as organized in a <i>single hierarchy</i> with properly nested lifetimes.<p>The function call stack becomes a call tree - each branch is a concurrent execution.
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.
So, my initial thought when reading was "yeah, it's async/await". But it's subtly different than that though.<p>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.<p>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.<p>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.<p>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.<p>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.<p>I do have a couple questions:<p>- 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.<p>- 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.<p>- Does this increase the hazard presented by non-halting async tasks?<p>[1] <a href="http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/" rel="nofollow">http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...</a>
This other post from the same blog gives some more concrete background on why this sort of structured concurrency is a good idea: <a href="https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/" rel="nofollow">https://vorpus.org/blog/some-thoughts-on-asynchronous-api-de...</a>
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.<p>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?
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:<p><pre><code> launch_thread(function, perhaps, some, initial, data);
</code></pre>
The trouble with this approach to concurrency is twofold:<p>(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.<p>(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.
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.<p>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.<p>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'.<p>They reflect the black box mentioned in the article, with one way in and one way out (e.g. when a method returns Future[_]).<p>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).<p>(yes, they are still leaky abstractions e.g. when debugging, but work fine most of the time)
Interesting ideas, and well-worded.<p>You could achieve something similar in JavaScript with Promise.all() and await:<p><pre><code> await Promise.all([
asyncFunc1(),
asyncFunc2(),
asyncFunc3()
])
</code></pre>
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.
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.
I was underwhelmed by reading this (maybe due to a clickbaity title and IMO sketchy extension of badness from goto to the go statement).<p>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.<p>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 <i>WAY</i> 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.
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.
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.)<p>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.
I found this pretty fascinating and I'm looking forward to hearing the theoreticians chew it over.<p>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:<p><pre><code> ...
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)
}
...
</code></pre>
And elsewhere:<p><pre><code> ...
func some_func(go) {
do_something()
go {
nursery {
// This nursery is within the outer one.
(go2) in
go2 { do_stuff }
go2 { do_more_stuff }
}
}
}
...</code></pre>
> 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.<p>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.<p>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.<p>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.
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.<p>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.
Is incredible how easy is to miss the point of this.<p>Let me try with something else.<p>Imagine you have try/catch/finally BUT NOT AS CONTROL FLOW CONSTRUCS but "just api calls".<p>So, you language need to be used like:<p><pre><code> foo()
exceptions.try{
bar()
this.catch{
}
}
</code></pre>
It means, you need to remember to ALWAYS REMEBER to "close" the start of the call.<p>Imagine how bad this could be. If only "try/catch" was as with "IF/ELSE/ENDIF" so you not do something stupid like:<p><pre><code> foo()
exceptions.catch{
what?()
}
bar()</code></pre>
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
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.
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.
The author might want to take a look at Habanero Java, which has all of this and more.<p><a href="https://wiki.rice.edu/confluence/display/HABANERO/Habanero-Java" rel="nofollow">https://wiki.rice.edu/confluence/display/HABANERO/Habanero-J...</a>
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.<p>cobegin/coend are limited to properly nested graphs, however fork/join can express arbitrary functional parallelism (any process flow graph) [1]<p>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.<p>[1] <a href="http://www.ics.uci.edu/~dillenco/compsci143a/notes/ch02.pdf" rel="nofollow">http://www.ics.uci.edu/~dillenco/compsci143a/notes/ch02.pdf</a>
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.
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.<p>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.
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.<p>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.