[HN Gopher] Conc: Better Structured Concurrency for Go ___________________________________________________________________ Conc: Better Structured Concurrency for Go Author : aurame420 Score : 81 points Date : 2023-01-11 20:48 UTC (2 hours ago) (HTM) web link (github.com) (TXT) w3m dump (github.com) | zdragnar wrote: | Is it just me or are the names and descriptions really confusing? | i.e. p.WithCollectErrored() configures result | pools to only collect results that did not error | TylerE wrote: | Think it's an error on the homepage. | | If you click through to the actual api doc it makes a lot more | sense: " WithCollectErrored configures the pool to still | collect the result of a task even if the task returned an | error. By default, the result of tasks that errored are ignored | and only the error is collected." | camdencheek wrote: | Whoops, yep, thanks for pointing it out. Just fixed it | camdencheek wrote: | Hi! Author here. Conc is the result of generalizing and cleaning | up an internal package I wrote for use within Sourcegraph. | Basically, I got tired of rewriting code that handled panics, | limited concurrency, and ensured goroutine cleanup. Happy to | answer questions or address comments. | zelphirkalt wrote: | Does this have anything to do with conc trees? | 082349872349872 wrote: | If as in Fortess, then no | camdencheek wrote: | Nope, just a catchy short form of "concurrent" | stephen123 wrote: | Great project. It seems like channels are just the wrong tool for | a lot of concurrency problems. More powerful than needed and easy | to get wrong. Lots of nice ways to make go concurrency safer. | | The problem that bothers me (and isnt in Conc), is how hard it is | to run different things in the background and gather the results | in different ways. Particularly when you start doing those things | conditionally and reusing results. | | Something like go-future helps. | https://github.com/stephennancekivell/go-future | camdencheek wrote: | > run different things in the background and gather the results | in different ways | | I'd be curious to see an example of the type of task you want | to be able to do more safely | openasocket wrote: | I think one of the examples they give is a bit misleading. This | func process(stream chan int) { var wg sync.WaitGroup | for i := 0; i < 10; i++ { wg.Add(1) go | func() { defer wg.Done() for elem | := range stream { handle(elem) | } }() } wg.Wait() } | | And func process(stream chan int) { p := | pool.New().WithMaxGoroutines(10) for elem := range stream | { elem := elem p.Go(func() { | handle(elem) }) } p.Wait() } | | Do slightly different things. The first one has 10 independent, | long-lived, go-routines that are all consuming from a single | channel. The second one has the current thread read from the | channel and dynamically spawn go-routines. They have the same | effect, but different performance characteristics. | chrsig wrote: | I haven't looked at the implementation at all, but it is | possible that the pool is keeping goroutines alive, and the | `Go()` method writes to a single `chan func()` that those | goroutines read off of. | | Which still isn't exactly equivalent, there's still an | additional channel read due to the `for elem := range stream | {}` loop, and likely an allocation due to the closure. | camdencheek wrote: | This is exactly correct. Behavior is equivalent, performance | is not. It's probably still not a great example because if | reading from a channel already, you're probably better off | spawning 10 tasks that read off that channel, but the idea of | the example was that it can handle unbounded streams with | bounded concurrency. | kevmo314 wrote: | The WaitGroup looks suspiciously like errgroup, which even has | the .WithMaxGoroutines() functionality: | https://pkg.go.dev/golang.org/x/sync/errgroup | | > A frequent problem with goroutines in long-running applications | is handling panics. A goroutine spawned without a panic handler | will crash the whole process on panic. This is usually | undesirable. | | In go land, this seems desirable. Recoverable errors should be | propagated as return values, not as panics. | alexeldeib wrote: | A goroutine created inside an http request handler (itself a | goroutine) which then panics, by default will crash the whole | server, not the single request. The panic could simply be an | out of bounds access. That should not crash the whole server. | | It's a logic bug, but you can't "not panic". You can trap and | recover it though. | | Bit orthogonal to OP but relevant to your reasoning. | morelisp wrote: | > simply be an out of bounds access | | If you have any care for quality at all, there's nothing | "simple" about your invariants being violated. | bxfhjcvu wrote: | [dead] | jerf wrote: | It is the way of things in an imperative language. If you catch | a panic, you are also declaring to the runtime that there is | nothing dangling, no locks in a bad state, etc. This is often | the case. (Although since I don't think this is a well- | understood aspect of what catching a panic means, it is | arguably only usually true by a certain amount of coincidence.) | But if you don't say that to the runtime, it can't assume it | safely and terminating the program, while violent, is arguably | either the best option or the only correct option. | | Other paradigms, like the Erlang paradigm, can have better | behaviors even if a top-level evaluation fails. But in an | imperative language, there really isn't anything else you | should do. It is arguably one of the Clues (in the "cluestick" | sense) that the imperative paradigm is perhaps not the one that | should be the base of our technology. But that's a well-debated | matter. | klodolph wrote: | I don't think this is really a question of whether your code | is imperative, since Haskell code will terminate just as | surely as Go code if you try to access an array element out | of range. | | (Haskell's lazy evaluation just makes it a bit harder to | catch, since you need to force evaluation of the thunk within | the catch statement, and it's far too easy to end up passing | your thunk to somebody who won't catch the exception.) | | As a matter of Go style, of course, you should almost always | defer unlock() after you lock(), but some people sometimes | get clever and think that they can just lock() and unlock() | manually without using defer. This is not hypothetical, and | it causes other problems besides leaving dangling locks after | a panic(). Somebody sticks a "return" between lock() and | unlock(), without noticing, for example. | | So my impression of catching panic() is that it is about as | safe as not catching panic(). What I mean by that is that if | recover() is not safe in your code base, there is a good | chance that there are other, related bugs in your code base, | and being a bit more strict about using defer and not trying | to be clever will go a long way. | morelisp wrote: | Since the Go 1.14 optimizations I don't believe I've found | a single case where `X(); defer Unx()` has been worse. | | Unfortunately this was not the case before 1.14, so there's | a lot of "middle-aged" code floating around setting a bad | example. | camdencheek wrote: | > The WaitGroup looks suspiciously like errgroup | | I heavily used errgroup before creating conc, so the design is | likely strongly influenced by that of errgroup even if not | consciously. Conc was partially built to address the | shortcomings of errgroup (from my perspective). Probably worth | adding a "prior art" section to the README, but many of the | ideas in conc are not unique. | | > In go land, this seems desirable. | | I mostly agree, which is why `Wait()` propagates the panic | rather than returning it or logging it. This keeps panics | scoped to the spawning goroutine and enables getting | stacktraces from both the spawning goroutine and the spawned | goroutine, which is quite useful for debugging. | | That said, crashing the whole webserver because of one | misbehaving request is not necessarily a good tradeoff. Conc | moves panics into the spawning goroutine, which makes it | possible to do things like catch panics at the top of a request | and return a useful error to the caller, even if that error is | just "nil pointer dereference" with a stacktrace. It's up to | the user to decide what to do with propagated panics. | morelisp wrote: | > This keeps panics scoped to the spawning goroutine | | This is exactly what's _un_ desirable. (IMO and from my | reading the GP agrees.) | avita1 wrote: | This isn't catching the panic though, this is propagating the | panic through the parent goroutine. The whole program will | still shut down, but the stacktrace that shows up in the panic | contains not only information information about the goroutine | that panicked, but also the launching goroutine. That can help | you figure out why the panic happened to begin with. | JamesSwift wrote: | Just took a glance but it seems like this is exactly the kind of | project I saw coming out of generics going live. I was really | surprised to see how subtly hard go concurrency was to do right | when initially learning it. Something like this that formalizes | patterns and keeps you from leaking goroutines / deadlocking | without fuss is great. | PaulKeeble wrote: | Its was initially something that surprised me about Go. It was | famous for good concurrency and yet compared to many functional | languages and contemporary OO languages it had a lot of foot | guns. There is a lot of repetition of code that even in | languages like Java had long been made common. Go seems to lack | obvious concurrency abstractions. | | When they announced generics the first thing I did with it was | rewrite my common slice parallel algorithm and my limited | concurrency pool. It is an obvious area needing improvement for | common use cases. ___________________________________________________________________ (page generated 2023-01-11 23:00 UTC)