Concurrency in Go
Concurrency in Go video course with in depth explanations & examples.
A go routines can block for one of these reasons:
- Sending/Receiving on channel
- Network or I/O
- Blocking System Call
Here's the full list of Go routines statuses:
- Gidle, // 0
- Grunnable, // 1 runnable and on a run queue
- Grunning, // 2 running
- Gsyscall, // 3 performing a syscall
- Gwaiting, // 4 waiting for the runtime
- Gmoribund_unused, // 5 currently unused, but hardcoded in gdb scripts
- Gdead, // 6 goroutine is dead
- Genqueue, // 7 only the Gscanenqueue is used
- Gcopystack, // 8 in this state when newstack is moving the stack
Feel free to check the rest of the statuses in the runtime source code
- Infinite loop — preemption (~10ms time slice)
- Local Run queue — preemption (~10ms time slice)
- Global run queue starvation is avoided by checking the global run queue for every 61 scheduler tick
- Network Poller Starvation Background thread poll network occasionally if not polled by the main worker thread
Here are couple of simple rules to make sure channels are used correctly
- Before writing to a channel, make sure someone else is reading from it (deadlock)
- Before reading from a channel, make sure someone else is writing to it (deadlock)
- When ranging over a channel, ALWAYS make sure the producer closes the channel eventually (deadlock)
- Writing to a closed channel will result in a runtime panic
- Reading from a closed channel won't have any effects
- A channel close, is considered a write operation
- Local Run queue
- Global Run queue
- Network Poller
- Work Stealing
The Coffman Conditions are known as the techniques/conditions to help detect, prevent and correct deadlocks.
The Coffman Conditions are as follows:
A concurrent process holds exclusive rights to a resource,
at any one time.
Wait for Condition
A concurrent process must simultaneously hold a resource
and be waiting for an additional resource.
A resource held by a concurrent process can only be released
by that process
A concurrent process (P1) must be waiting on a chain of other
concurrent processes (P2), which are in turn waiting on it (P1)
Primarily the Go scheduler has the opportunity to get triggered on these events:
- The use of the keyword go
- Garbage collection
- System calls (i.e. open file, read file, e.t.c.)
- Synchronization and Orchestration (channel read/write)
P, M, G
Once the syscall exists Go tries to apply one of the rules:
- try to acquire the exact same P, and resume the execution
- try to acquire a P in the idle list and resume the execution
- put the goroutine in the global queue and put the associated M back to the idle list
Goroutines do not go in the global queue only when the local queue is full;
it is also pushed in it when Go inject a list of goroutines to the scheduler,
e.g. from the network poller or goroutines asleep during the garbage collection
sysmon is smart enough to not consume resources when there is nothing to do.
Its cycle time is dynamic and depends on the current activity of the running program.
The initial pace is set at 20 nanoseconds, meaning the thread is constantly looking to help.
Then, after some cycles, if the thread is not doing anything, the sleep between two cycles
will double until it reaches 10ms.
If your application does not have many system calls or long-running goroutines,
the thread should back off to a 10ms delay most of its time, giving
a very light overhead to your application.
The thread is also able to detect when it should not run. Here are two cases:
- The garbage collector is going to run. sysmon will resume when the garbage collector ends.
- All the threads are idle, nothing is running.
Here's how Go makes sure to equally distribute & balance work
and make use of computer resources as efficient as possible:
- pull work from the local queue
- pull work from the global queue
- pull work from network poller
- steal work from the other P’s local queues
GOMAXPROCS=2 GODEBUG=schedtrace=1000,scheddetail=1 go run main.go
In general terms a pipeline is a mechanism for inter-process communication using message passing,
where the output of a pipeline is the input for the next pipeline.
Suppose that assembling one car requires three tasks that take 20, 10, and 15 minutes, respectively.
Then, if all three tasks were performed by a single station, the factory would output one car every 45 minutes.
By using a pipeline of three stations, the factory would output the first car in 45 minutes,
and then a new one every 20 minutes.