I wish I could be satisfied that all THE people I meet and all the things I do are just a little bit better.
This article reprinted from: blog.lab99.org/post/golang…
I. Video information
1. Video viewing address
www.youtube.com/watch?v=KBZ…
2. PPT download address
Download.csdn.net/download/xu…
3, post
About.sourcegraph.com/go/understa…
2. Concurrency features of Go
- Goroutines: Executes each task independently and possibly in parallel
- Channels: Used for communication and synchronization between channels
1. A simple transaction example
For non-concurrent programs such as:
func main() {tasks := getTasks() // Process each taskfor _, task := range tasks {
process(task)
}
}
Copy the code
Switching to Go’s concurrent mode is easy, using the typical Task Queue mode:
func main() {// create channel ch := make(chan Task, 3) // run a fixed number of workersfori := 0; i < numWorkers; I++ {go worker(ch)} // send tasks to workers := getTasks()for _, task := range hellaTasks {
ch <- task
}
...
}
func worker(ch chan Task) {
forTask := <-ch process(task)}}Copy the code
2. Characteristics of Channels
- Goroutine-safe. Multiple Goroutines can access a channel at the same time.
- Can be used to store and pass values between goroutines
- The semantics are first in, first out (FIFO)
- Can result in block and unblock of goroutine
Three, parsing,
1. Construct a channel
// channel ch := make(chan Task, 3)Copy the code
Review the characteristics of channels mentioned earlier, especially the first two. What if you ignored the built-in channel and were asked to design something with Goroutines-safe that could be used to store and pass values? A lot of people might think maybe you can do it with a locked queue. Yes, in fact, a channel is a queue with a lock inside it. Golang.org/src/runtime…
typehchan struct { ... Buf unsafe.Pointer // to a ring queue... Sendx uint // send index recvx uint // Receive index... Lock mutex // mutex}Copy the code
The implementation of BUF is very simple, which is the implementation of a ring queue. Sendx and recvx are used to record the locations of sending and receiving respectively. A LOCK mutex is then used to ensure uncontested risk.
For each ch := make(chan Task, 3) operation, a space is allocated in the heap to create and initialize a hchan variable, and ch is a pointer to the hchan.
Since ch is a pointer, it is possible to pass ch through a goroutine call without taking a pointer to ch, so all goroutines that use the same ch point to the same actual memory space.
2. Send and receive
For the convenience of description, we use G1 to represent the goroutine of main() function, while G2 represents the goroutine of worker.
// G1
func main() {...for _, task := range tasks {
ch <- task
}
...
}
// G2
func worker(ch chan Task) {
for {
task :=<-ch
process(task)
}
}
Copy the code
2.1 Simple Send and receive
What does ch < -task0 do in G1?
- Acquiring a lock
- Enqueue (task0)
- Release the lock
This step is easy, but let’s see how t := < -ch of G2 reads the data.
- Acquiring a lock
- T = dequeue() (again, memory replication)
- Release the lock
This step is also very simple. However, as we can see from this operation, all goroutines share only the hchan structure, and all communication data is an in-memory copy. This follows an idea that is central to Go concurrent design:
Do not communicate by sharing memory; instead, share memory by communicating
Memory replication refers to:
// typedmemmove copies a value of type t to dst from src.
// Must be nosplit, see # 16026.
//go:nosplit
func typedmemmove(typ *_type, dst, src unsafe.Pointer) {
if typ.kind&kindNoPointers == 0 {
bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.size)
}
// There's a race here: if some other goroutine can write to // src, it may change some pointer in src after we've
// performed the write barrier but before we perform the
// memory copy. This safe because the write performed by that
// other goroutine must also be accompanied by a write
// barrier, so at worst we've unnecessarily greyed the old // pointer that was in src. memmove(dst, src, typ.size) if writeBarrier.cgo { cgoCheckMemmove(typ, dst, src, 0, typ.size) } }Copy the code
3. Block and restore
3.1 The sender is blocked
Suppose THAT G2 takes a long time to process, during which TIME G1 keeps sending tasks:
ch <- task1
ch <- task2
ch <- task3
Copy the code
However, the next time ch < -task4, since the CH buffer is only 3, there is no place to put it, so G1 is blocked, and when someone removes a Task from the queue, G1 is restored. We all know that, but what we care about today is not what happened, but how did it happen?
3.2 Run time scheduling of Goroutine
First, goroutine is not an operating system thread, but a user-space thread. Therefore, Goroutine is created and managed by the Go Runtime, not the OS, and is lighter than operating system threads.
Of course, the Goroutine eventually runs in a thread, and the scheduler in the Go Runtime controls how the goroutine runs in a thread.
Go’s runtime scheduler is the M:N scheduling model, where N Goroutines run on M OS threads. In other words, it is possible to run multiple Goroutines in an OS thread.
Three structures are used in the M:N scheduling of Go:
- M: OS threads
- G: goroutine
- P: scheduling context
- P has a runqueue of all runnable Goroutines and their contexts
3.3 The specific process of goroutine blocking
When ch < -task4 is executed, the channel is already full and needs to be paused G1. At this time:
- G1 calls the run-time gopark
- Then Go’s run-time scheduler takes over
- Change G1’s state to Waiting
- The connection between G1 and M is switched out, so THAT G1 disconnects from M. In other words, M is idle and can schedule other tasks.
- From the run queue of P, get a runnable Goroutine G
- Establish a new relationship between G and M (Switch in), so G is ready to run.
- When the scheduler returns, the new G will run, and G1 will not run, which is the block.
As you can see from the flow above, for Goroutine, G1 is blocked and a new G is running; For the operating system thread M, it is not blocked at all.
We know that OS threads are much heavier than Goroutine threads, so try to avoid OS thread blocking here to improve performance.
3.4 Specific process of goroutine restoration
Now that you’ve understood blocking, let’s understand how to resume running. However, before we can continue to understand how to recover, we need to take a step forward to understand the structure of Hchan. Because how does the scheduler know which Goroutine to keep running when the channel is not full? And how does Goroutine know where to get the data?
In hCHAN, in addition to the previously mentioned content, there are also defined two queues, sendQ and RecVq, which respectively represent the Goroutine waiting to send and receive, and related information.
typehchan struct { ... Buf unsafe.Pointer // to a ring queue... Sendq waitq // Queue waiting for sending recvq waitq // Queue waiting for receiving... Lock mutex // mutex}Copy the code
Waitq is a queue with a linked list structure, and each element is a sudog structure, whose definition is roughly as follows:
typeStruct {g *g; // goroutine elem unsafe.Pointer; // struct {g *g; }Copy the code
Golang.org/src/runtime…
So during the previous blocking of G1, in fact:
- G1 creates a sudog variable for itself
- It is then appended to sendq’s wait queue so that a future receiver can use the information to recover G1.
This all happens before the scheduler is called.
So let’s start with how to recover.
When G2 calls t := < -ch, the channel state is full, the buffer is full, and G1 is in the waiting queue. G2 then performs the following operation:
- G2 first executes dequeue() to retrieve task1 from the buffered queue to T
- G2 pops up a sudog waiting to be sent from Sendq
- Enqueue (), the value of elem in the pop-up sudog, to buF
- Change the state of goroutine (G1) in the pop-up sudog from Waiting to Runnable
- G2 then needs to notify the scheduler that G1 is ready for scheduling, so call goReady (G1).
- The scheduler changes G1’s state to Runnable
- The scheduler pushes G1 into P’s run queue, so that at some point in the future when scheduled, G1 will start running again.
- Return to the “G2”
Note that G2 is responsible for pushing G1’s ELEM into BUF, which is an optimization. This eliminates the need to acquire the lock, enqueue(), and release the lock again when G1 resumes. This avoids the overhead of multiple locks.
3.5 What if the receiver blocks first?
What’s even cooler is the process where the receiver blocks first.
If G2 executes t := < -ch first, buf is empty, so G2 will block.
- G2 creates a sudog structure variable for itself. Where G is itself, G2, and elem refers to T
- Push the sudog variable into the RECVQ wait receive queue
- G2 needs to tell Goroutine that it needs pause, so it calls gopark(G2).
- As before, the scheduler changes its G2 state to Waiting
- Disconnect G2 from M
- Fetch a Goroutine from the run queue of P
- Establish a new goroutine and M relationship
- Returns to continue running the new Goroutine
This should be familiar, but what does the flow look like when G1 starts sending data?
G1 can queue(task) and then call goReady (G2). But we can be smarter.
According to the status of hchan structure, we already know that after task enters BUF, G2 will read its value and copy it to T after it recovers. So G1 can skip buF at all, and G1 can just give the data to G2.
Goroutines generally have their own stacks and do not access each other’s stack data, except for channels. Here, since we already know the address of t (via the ELEm pointer), and since G2 is not running, we can safely assign directly. When G2 recovers, there is no need to acquire the lock again, nor do operations on buF. This saves the overhead of memory replication and lock operations.
4, summarize
- goroutine-safe
- Lock Mutex in hchan
- Store, transfer values, FIFO
- Through the ring buffer in Hchan
- Causes goroutine to block and recover
- Sendq and Recvq in Hchan are linked list queues of sudog structure
- Call the run-time scheduler (gopark(), goReady ())
4. Operation of other channels
1. Unbuffered channel
An unbuffered channel behaves like the direct send example above:
- Receiver block → sender writes directly to receiver’s stack
- The sender block → accept method reads directly from the sender’s sudog
2, select
Golang.org/src/runtime…
- Lock all channels that need to be operated on
- Create a sudog for yourself and add sendQ or RecvQ to all channels (depending on whether you send or receive)
- Call select goroutine (gopark())
- The goroutine of the select is then scheduled to execute when any channel is available.
- resuming mirrors the pause sequence
5. Why is Go designed like this?
1, Simplicity prefers locked queues over lock-free implementations.
Performance improvements do not come out of thin air, but as complexity increases.
Dvyokov the latter may perform better, but this advantage does not necessarily outweigh the disadvantage brought by the complexity of the implementation code.
2, the Performance,
- Call the Go runtime scheduler to keep OS threads from blocking stack reads and writes across the Goroutine.
- This allows Goroutine to wake up without having to acquire the lock.
- Some memory replication can be avoided.
Of course, any advantage has its costs. The trade-off here is implementation complexity, so there are more complex memory management mechanisms, garbage collection, and stack shrinking mechanisms.
Here the performance gains outweigh the complexity gains.
The tradeoff of simplicity vs. performance can be seen in various code for channel implementations.
6. Information of the blogger
Personal wechat official Account:
Personal blog
Individuals making
Personal gold digger blog
Personal CSDN blog