tags : Concurrency, Golang

100% chances of things being wrong here.

Go concurrency tips

Detecting data races

  • If two goroutines try to modify a shared variable, we have a race condition.
  • go has a built-in data race detector which can help us detect it. See Race Detector.

Runtime and Scheduler other parts

Go runtime creates OS threads. The Go scheduler schedules goroutines on top of these OS threads.

Go runtime

Go scheduler

Initial attempts

  • 1:1 threading: 1 goroutine. To expensive. syscalls, memory, no infinite stacks.
  • Thread pools: faster goroutine creation. But more memory consumption, performance issues, no infinite stacks.

M:N threading

  • We multiplex goroutines on the threads
  • If we have 4 OS threads, they can run 4 goroutines at the same time

M:N Scheduler

  • Because OS doesn’t know anything about our goroutines, we need to come up w our own scheduler

  • We need to keep track of

    • Run Queue: Runnable Goroutines
  • We don’t need to keep track of Blocked goroutines as channels have their own wait queue. When we unblock a goroutine, it’s moved to the scheduler run queue.

  • Syscalls

    • When a goroutine does a syscall, and things go into the kernel we have lost control over the goroutine. We’ll only know if it returns back.
    • If all threads are doing syscalls, then we can’t even schedule new stuff now. And if one of a runnable goroutine holds the lock, we have a artificial deadlock. This issue is common for any scheduler w fixed number of threads.
    • Solution to this, in the last thread, it’ll start another os thread, which will start re-flow of the runqueue
      • If we have 5 threads, we only want to run 4

Distributed M:N Scheduler

  • M:N Scheduler doesn’t scale as multiple threads try to access just one mutex
  • So we give each thread its own M:N scheduler
  • Which distributed scheduler, we now have the problem of having an unified view of the Run Queue.
    • Local run-queue ⇒ Global run-queue ⇒ Steal work of other schedulers
    • So we also got some load balancing now
  • This still not enough because checking for work stealing involves syscalls and now each thread needs to syscalls and will result in cpu being busy for nothing.

Go Processor M:P:N

  • P: GOMAXPROCS env var
  • go_processor: A artificial resource required to run Go code. No. of go_processor objects == cores.
  • If goroutine is executing syscall, it does not need a go_processor, it’s only needed for goroutines running go code.
  • With this, threads only need to check fixed number of go_processor. So work stealing becomes scalable.

Go GC

Concurrency in go, what are my options/primitives?

  • There are no Threads. But we have some sort of Coroutines + Green Threads combination. They’re called goroutines. You probably will interact with os threads w cgo ig.

Communication

  • Shared state: Atomics, Mutex and friends
  • Message passing: channels

Synchronizations

  • You can always use shared state for synchronization. Can also go lockfree maybe.
  • Channels too can be used for synchronization, as reading from channel is a blocking call.

When to use what?

Some general guidelines

  • Understand what data structure you’re working w. Eg. Go Maps are not goroutine-safe, you need to use a RWMutex if you use it to share data across goroutines.
  • Try avoiding shared state, if cannot avoid, deal things at max w one mutex.
  • If you take a lock in your function, and call something else that takes a lock, that’s two locks. If you need two locks might aswell switch to channels. (Not my words, I don’t even understand this deeply but sounds wise, have to think more why this is the way it is)

goroutines

  • go keyword is the only api for goroutines
  • Users can’t specify stack size for goroutines.
  • When a go-routine
    • sleeps/blocked: underlying thread can be used by another go-routine.
    • wakes up: It might be on a different thread.
    • Go handles all this behind the scenes.

Benefits over kernel threads

  • w M:N scheduling & channels, we get efficiency due low overhead in context switching.
  • Lower memory allocation per go routine vs os threads

goroutine-safe/thread-safe data structure(X)?

  • See Goroutine-safe vs Thread-safe
  • See Threads
  • goroutine-safe is not well-established term. While thread-safe is, and it means safe for concurrent access.
  • You can freely substitute “thread-safe” with “goroutine-safe” in Go docs, unless documentation very explicitly refers to actual threads
  • Here X can be some data structure.
  • If X can be accessed by multiple goroutines without causing any conflict, we can say X is goroutine safe. X itself manages the data and ensures the safety of concurrent access.
  • Eg. Go’s channel are a goroutine-safe FIFO object.

Channel

  • Channel is a queue(FIFO) with a lock. It’s goroutine safe.
  • Multiple writers/readers can use a single channel without a mutex/lock safely.
  • When we get a channel, we essentially get a pointer to the channel.
  • Good to pass a channel as a parameter. (You have control over the channel)
  • Closing channels
    • Closing channels is not necessary, only necessary if the receiver must know about it. Eg range on the channel.
    • Always close channels from sending side.
  • Multiple channels
    • select, this is similar to unix select(2), hence the name.
    • It’s often used inside an infinite loop (say, in a consumer) to grab data from any available channel.
    • chooses one at random if multiple are ready.

Implementation

  • Defined by the hchan struct, at runtime this is allocated on the heap.
  • Enqueue and Dequeue are “memory copy”. This gives us memory safety. Because, no shared memory except the hchan and hchan is protected by its lock.

Unbuffered (Synchronous)

ch := make(chan int)
ch := make(chan int, 0) // same as above

  • Combines communication w synchronization

Buffered (Asynchronous)

ch := make(chan int, 3)
// ch := make(chan int, x>0)

  • Don’t use buffer channels unless you’re sure you need to
  • Can be used as a semaphor, eg. limit throughput in a webserver
  • if we want the producer to be able to make more than one piece of Work at a time, then we may consider buffered

Blocking in buffered and unbuffered

func main() {
    ch := make(chan int, 1)
    ch <- 32 // if unbuf, things will not move post this! deadlock.
	// ch <- 88 // this will block an buf(1) ch! (channel full)
    <-ch     // if unbuf, this must be called from another goroutine
    fmt.Println("Hello, 世界")
}
  • Buffered
    • S: Until the receiver has received the value
    • R: Blocks if nothing to receive
  • Unbuffered
    • S: Blocks when the buffer is full, something must receive now.
    • R: Blocks if empty buffer

Resources

More resources