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