Skip to content

Latest commit

 

History

History

concurrency

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

Concurrency

Goroutines

Every function spawned with go keyword creates new goroutine, e.g.:

go fn(a, b, c)

Check a tour of Go

Before spawning a new goroutine(the checklist):

  1. Avoid Concurrency.

    Concurrency in Go is not a silver bullet, it wouldn't make code faster in all cases.

    Async code harder to understand and debug is. Hrrmmm.

    Master Yoda

    e.g. there is no speed benefit in this example for one CPU, and async solution consumes 50%100% more memory.

    Another example:

    var wg sync.WaitGroup
    
    wg.Add(1)
    go serve(&wg)
    wg.Wait()

    vs.:

    serve()

    Also, Leave concurrency to the caller

  2. Make sure, that there would be a limited amount of running goroutines. Use one of next:

    • use errgroup with SetLimit()
    • use semaphore or just some make(chan struct{}, N) before spawning a new goroutine.
    • use pool from conc with WithMaxGoroutines
    • create worker pool. Shouldn't be used in most cases since there would be hanging workers, that are doing nothing, and spawning a new goroutine is quite cheap.
    • etc., e.g. HTTP server has connections limit.
  3. It should be possible to stop running goroutines, e.g. on service shutdown or HTTP timeout. Use one of next for cancellation:

    • context.Context(Context is cancelled or the deadline is exceeded. Preferable.)
    • stop channel(kinda legacy solution and hard to use with other APIs, e.g. with some DB ORM or HTTP clients)

    Also write context aware code.

    HTTP timeout example:

    func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context() // http.Server should have timeouts
    
        var entity Entity
        _ = json.NewDecoder(r.Body).Decode(&entity) // Don’t forget to check the error and close the body
    
        group, _ := errgroup.WithContext(ctx)
        group.Go(func() (err error) {
            return h.db1.CreateEntity(ctx, entity) // a lot of methods takes context, not stop chan 
        })
        group.Go(func() (err error) {
            return h.db2.TransactionLog(ctx, entity)
        })
    
        err := group.Wait() // blocking
        if err != nil {
            http.Error(w, "db error", http.StatusInternalServerError)
            return
        }
    
        w.WriteHeader(http.StatusNoContent)
    }
  4. Never start a goroutine without knowing when it will stop.

    e.g. on shutdown app should wait for all goroutines to stop.

    Use one of next:

    See also: when you spawn goroutines, make it clear when or whether they exit.

  5. Panics. Recover could catch panic only in current goroutine, so make sure, that panic is handled in goroutine.

    Non-catchable example:

    func f1() {
        if true {
            panic("f1")
        }
    
        fmt.Println("f1 done")
    }
    
    func main() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("recovered panic:", r)
            }
        }()
    
        go f1()
    
        for i := 0; i < 10; i++ {
            fmt.Println("tick", i)
            time.Sleep(time.Second)
        }
    }

    Also, panic could block goroutine forever, so call mu.Unlock, wg.Wait or close(ch) in defers.

  6. wg.Add(...) should be called before running goroutine.

To sum up. The easiest ways.

Tips and tricks

Channels

Channels are a typed conduit through which you can send and receive messages.

Check a tour of Go

Commandments

  1. Consider using shared memory instead of message passing:

    Our study found that message passing does not necessarily make multithreaded programs less error-prone than shared memory. In fact, message passing is the main cause of blocking bugs. To make it worse, when combined with traditional synchronization primitives or with other new language features and libraries, message passing can cause blocking bugs that are very hard to detect. Message passing causes less nonblocking bugs than shared memory synchronization and surprisingly, was even used to fix bugs that are caused by wrong shared memory synchronization. We believe that message passing offers a clean form of inter-thread communication and can be useful in passing data and signals. But they are only useful if used correctly, which requires programmers to not only understand message passing mechanisms well but also other synchronization mechanisms of Go.

    Understanding Real-World Concurrency Bugs in Go

  2. Do not send into closed channel. However, reading from closed channel is OK.

  3. Do not send to or receive from a nil channel it will block forever.

  4. Don't make huge buffered channels. Channel is just a data buffer, don't try to feet all results there(you would either make it too small and block on writing, or make it too big and use redundant memory). Prefer channels with a size of zero or one. Most other sizes are guesses.

  5. Channel consumer should write values into DB/cache/file/socket/map/slice/other data structures.

  6. If channel producer(writer) is not running in goroutine the channel consumer should be spawned before it.

  7. Channel should be closed once either by the producer(if it's one) or with the help of sync.WaitGroup/sync.Once(if there are many producers).

More

  • Egon Elbre - Production Ready Concurrency(read and watch)