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
- Watch: Dmitry Vyukov — 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. ofgo_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. Whilethread-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 sayX
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 thechannel
. - 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.
- Closing channels is not necessary, only necessary if the receiver must know about it. Eg
- Multiple channels
select
, this is similar to unixselect(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 theheap
. - Enqueue and Dequeue are “memory copy”. This gives us memory safety. Because, no shared memory except the
hchan
andhchan
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
- The Behavior Of Channels
- Share memory by communicating · The Ethically-Trained Programmer
- Internals of Go Channels. Inner workings of Go channels