[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)