19 ms·
Notes on structured concurrency, or: Go statement considered harmful
- pnathan 8y agoArticle 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.
- lilyball 8y ago> 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.
- pnathan 8y agoI hesitate to say that's all they did. There's a context & concept of structuring concurrency that they are building.
- coldtea 8y agoIn the sense that a car is a reinvention of a horse buggy.
- eridius 8y agoIn 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.
- m0meni 8y agoI 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 8y agoI 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.
- scott_s 8y ago> 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! His section "There is an escape" answers this criticism: "The nursery object also gives us an escape hatch. What if you really do need to write a function that spawns a background task, where the background task outlives the function itself? Easy: pass the function a nursery object." He goes on to explain a bit more, but one of his points is what really ties this abstraction back into the goto discussion: "Since nursery objects have to be passed around explicitly, you can immediately identify which functions violate normal flow control by looking at their call sites, so local reasoning is still possible." > 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. I think they can: applications with background threads would have an outer-level nursery at whatever "main" is for them, and that nursery would be passed into whatever function needs to spawn a background thread.
- egnehots 8y agoThere are other approaches. State machines, reactors are often used in low level layers and the Actor model for some high level design.
- mindB 8y agoWhen 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.
- cousin_it 8y agoDidn'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 8y agoI 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 8y agoMaybe 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 8y ago"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.
- lclarkmichalek 8y agoThis 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).
- shabbyrobe 8y agoThank you! I had no idea about this package! I rolled my own similar thing, but I'd much rather be using something semi-blessed. Looks like I have some refactoring to do, but that's not always a bad thing.
- arghwhat 8y agoAh, neat. It's relatively new—I've always rolled my own implementation whenever I needed it (it's super simple, which is why this library is 67 lines of implementation, half being comments), but why roll your own when it's part of the (experimental) standard library?
- sagichmal 8y agohttps://godoc.org/github.com/oklog/run https://godoc.org/github.com/oklog/run is a more-general and IMO more-elegant version of errgroup.
- erik_seaberg 8y agoBe advised that errgroup doesn't recover from panics to prevent them from killing the process.
- atombender 8y agoErrGroup 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.
- shadowmint 8y agoThis 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.
- munjal116 8y agoOr Promise.all/Promise.reject for that matter.
- pcwalton 8y agoPure fork/join concurrency isn't a novel idea. I was doing some work on it in grad school. Without blocking channels, fork/join has some really nice properties, such as guaranteed deadlock freedom. However, I agree with you that there's no one-size-fits-all approach to concurrency. Sometimes you want long-lived tasks that communicate—the actor model, in other words—in which case fork/join doesn't buy you much. I do think that fork/join is frequently what you want, though.
- joshuamorton 8y ago>this is distinct from a normal awaited block which will execute tasks inside it sequentially, awaiting each one in turn I believe that python's `async for` construct (which may be stolen from c#, but I'm not sure) does this, and another user mentioned Promise.all which in javascript I believe allows the child promises to resolve concurrently. In both cases, these are for concurrency, not parallelism.
- wokwokwok 8y agoThe point of the construct is not explicitly waiting for child tasks/threads/whatever, but that as a formal restriction, you cannot leave the scoped block with unresolved tasks. I don't believe either of the constructs you've mentioned do this, and I'm not aware of any that do. The trivial counter example would be a deeply nested `setTimeout(..., 5000)` inside the javascript code. It doesn't matter if you've called Promise.all or not. Also, regarding parallelism: Obviously async/await are for that; this is basically pitching an equivalent construct for parallel processing. I think this is novel, frankly, and the alternatives being thrown around by people are by people who didn't read the article.
- woah 8y agoWorst 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 8y agoI think this article is a lot more than just "someone pushing their library" It is well organized, written and specific about it's claims.
- xfer 8y agoHow is this different than calling join(), after spawn()?
- jerf 8y agoHow 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 8y agobuild 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 8y ago> If i want a blocking thread i would just call a function That is not what this is though.
- wilun 8y agoGiven the "escape hatch", there is no difference to passing a thread pool object around (or referencing it otherwise), that would for ex join on destruction. And this is a valid approach. But pretending that this should be the only one and that this is in a way similar to unstructured goto vs structured programming? I'm not buying it. Because there will be long lived global nurseries floating around in big enough codebases, effectively eliminating all the guarantees they are supposed to provide for the affected threads. I mean; I'm not sure they can even guarantee the advantages they are supposed to provide (in the sense of providing new easy to check properties, with actual tools existing capable of checking them). Don't get me wrong. I find the approach interesting, and will happily use it where applicable, but just the comparison to goto does not really makes sense, nor does the fiction that threads are best modeled by always being contained into managing function calls (hmf, except when they are not...). The "escape hatch" is so big that it just plain devalues the solution compared to not having it (or having it only in vastly more constrained ways) and then obviously not pretending this is what should replace traditional spawning (and even more) everywhere.
- panic 8y agoThis 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-design-in-a-post-asyncawait-world/ https://vorpus.org/blog/some-thoughts-on-asynchronous-api-de...
- bitL 8y agoThat's pretty cool! Thanks for enriching my day and good luck with your framework!
- amluto 8y agoI’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.
- arghwhat 8y agoWhat. 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 8y agoI 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 8y agoKnowing 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.
- titzer 8y agoThe 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-Java https://wiki.rice.edu/confluence/display/HABANERO/Habanero-J...
- jaffee 8y agoI 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!
- igolden 8y agoI'm not quite won over, but the links to Dijkstra's work were a goldmine for me! Totally agree
- acjohnson55 8y agoI think people are missing the merits of what the author said in their defensiveness of their existing approaches, which is a bit sad.
- tomtheelder 8y agoThe 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!
- deleted 8y ago[deleted]
- jey 8y agoI'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.
- bm1362 8y agoIn Go you can use waitGroups to ensure that the goroutines are completed, similar to join. You’ll have to pass a cancel chan or context if you’d like to cancel/timeout.
- pknopf 8y agoWith that approach, you increase the number of threads being used for a single "block" by how deep your "wait" stuff goes. In C# and Python, an async context shares a single thread loop that all joins jump back to. Simply starting a thread and waiting on it isn't suitable in most scenarios, namely web and UI dev. I wish Go had something like this as well.
- nemothekid 8y agoIn Go it doesn't really matter how many "threads" you have because Go has its lightweight threading model (aka coroutines) - creating a new "thread" is very cheap. The reason C#, Python (and Rust), have that model is because they don't have coroutines and starting another thread is very expensive.
- pknopf 8y agoThe built in thread pools must have a limit. Also, this isn't just a matter of keeping thread resources down, it's supporting scenarios like developing web and UI software. For example, if I have "OnButtonClick" and it does async work, I'd like to also call other functions that may also touch UI components. WPF has support for binding async actions to UI events, and it eliminates the need to ever wonder what thread you are on. In short, you can't "join" a thread on the UI thread. Proper async/await with a task scheduler is invaluable. Also, C# has thread pools. Most people don't create a raw thread using "new Thread()". They would normally use "Task.Factory" or "Task.Run". If it is a long running thread, you can specify to Task.Factory that is it long running and it will remove your thread from the thread pool and put another one in it's place.
- bgorman 8y agoI 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 8y agoClojure 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.
- erpellan 8y agoThe 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.
- jimmoores 8y agoThis is realised in a real language as occam. I've never understood why so few languages have a par construct, it's much more natural than asynchronous launching.
- lou1306 8y agoThe neat thing is that PAR and SEQ do not compose mere operations; they compose other CSP processes. This gives CSP a very regular, self-similar structure. (The "basic" processes are just the send/receive of a value over a channel. By the way, Go channels are a direct lift from CSP!)
- m0meni 8y agoThis 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 8y agoChannels are lifted from CSP, but PAR is obviously missing.
- 8y ago
- ptero 8y agoI 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.
- dingo_bat 8y agoI 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.
- liveoneggs 8y agohttp://rvirding.blogspot.com/2008/01/virdings-first-rule-of-programming.html http://rvirding.blogspot.com/2008/01/virdings-first-rule-of-...
- Fellshard 8y agoA 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 8y agoI 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.).
- leshow 8y ago> 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 8y agoSomewhat 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 8y agoNot 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.
- themihai 8y agoWell they lost me at the callbacks... if go statements are harmful and callbacks are not then I rather prefer the harmful way.
- vishvananda 8y agoYou 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.
- ychen306 8y agoHmm.. Looks like the author is reinventing fork-join style parallelism...
- coldtea 8y agoLooks like people skim articles and apply anything they already know and recognize to explicitly more evolved ideas...
- ufmace 8y agoI 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 8y ago> 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.
- _Codemonkeyism 8y agoThe 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 8y agoI 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.
- z0r 8y agoWouldn't the unreferenced Future, once fulfilled, be garbage collected?
- _Codemonkeyism 8y agoYes
- _Codemonkeyism 8y agoIf the future has a side effect I'm not concerned with, like sending a mail, I can't see the problem?
- acjohnson55 8y ago
- acjohnson55 8y agoSo, 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-your-function/ http://journal.stuffwithstuff.com/2015/02/01/what-color-is-y...
- gafferongames 8y agoThis is absurd. The only thing "go" has in common with "goto" is the first two letters.
- Animats 8y agoThis 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.
- nnq 8y ago> Even the original examples had shared data. But the language didn't provide much support for controlling that sharing. Can you elaborate on this?
- Animats 8y agoYes, but there will be a service charge.
- rumcajz 8y agoAlmost 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.
- earenndil 8y agoHis 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.
- pmarreck 8y agoI 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 8y agoI 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.
- bvinc 8y agoIt's a place for threads to run... a sweater? A sewing machine? A spool?
- mattnewport 8y agoYeah, I thought the article was interesting but the choice of "nursery" still has me scratching my head. I can't see where it comes from or why that word was chosen.
- _bxg1 8y agoInteresting 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.
- acjohnson55 8y agoI had a similar thought, initially, which I posted elsewhere in this submission: https://news.ycombinator.com/item?id=16924407 https://news.ycombinator.com/item?id=16924407
- Osiris 8y agoThat'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.
- Osiris 8y ago> 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.
- deleted 8y ago[deleted]
- catnaroek 8y agoLately, 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.
- jnwatson 8y agoWhat 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.
- coldtea 8y agoIt'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 8y agoIf 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_threadpool/struct.Pool.html http://kimundi.github.io/scoped-threadpool-rs/scoped_threadp... EDIT: He compares to Rust in a comment on the Reddit thread: https://www.reddit.com/r/programming/comments/8es8x3/notes_on_structured_concurrency_or_go_statement/dxxucgi/?st=jggijgyw&sh=3a0dced9 https://www.reddit.com/r/programming/comments/8es8x3/notes_o... 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.
- imauld 8y agoHow did someone manage to write a post that long discussing Go and it's concurrency model and not once mention channels?
- shalabhc 8y ago"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.
- glorioustao 8y agogo 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
- zzzcpan 8y agoI 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.
- atrn 8y agoComing 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.
- benjohnbarnes 8y agoI 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 } } } } ...
- mamcx 8y agoIs 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()