Goroutine’s multi-core parallelization gives up time slices

Time:2021-12-2

1. Multi core parallel

Starting from go 1.5, go’s gomaxprocs default value has been set to the number of CPU cores, which allows our go program to make full use of each CPU of the machine and improve the concurrency performance of our program to the greatest extent.

runtime.GOMAXPROCS(16)

How many CPU cores should be set? In fact, another function numcpu () is provided in the runtime package to get
Take the number of cores. It can be seen that the go language has actually sensed all the ring information. In the next version, we can use this information to schedule goroutine to all CPU cores, so as to maximize the multi-core computing power of the server. It is only a matter of time before gomaxprocs is abandoned.

    fmt.Printf("runtime.NumCPU(): %v\n", runtime.NumCPU()) //runtime.NumCPU(): 12

Look at the following simple code

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    fmt.Println("B")
}
func main() {
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

It is found that the outputs of a and B are irregular and random
Operation results:

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
A
B
A
A
B
A
B
A
B
A
B
B
B
A
A
B
A
A
B
B
[Done] exited with code=0 in 0.364 seconds

It is proved that the two go processes opened in each cycle are parallel, because in my version, the default value of gomaxprocs of the default go has been set to the number of CPU cores. If I set gomaxprocs of go to 1, it means that these goroutines are running on a CPU core. When one goroutine gets a time slice for execution, other goroutines will be in a waiting state

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

After setting runtime. Gomaxprocs (1), run again. No matter how many times you run, there are two alternating outputs, which is very regular

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
[Done] exited with code=0 in 0.353 seconds

Because the go process created in each cycle is on the same CPU core, there is only one p queue corresponding to the GPM model, which needs to be queued for execution, so the above output results appear

2. Give up the time slice

We can control when to actively transfer time slices to other goroutines in each goroutine, which can be realized by using the gosched () function in the runtime package.
In fact, if you want to control the behavior of goroutine more finely, you must have a deeper understanding of the specific functions provided by the runtime package in the go language development package.

We still modify the above code: (similarly, when the gomaxprocs of go is 1, the goroutine P queue is 1, and multiple go processes cannot be parallel)

package main

import (
    "fmt"
    "runtime"
    "sync"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    Runtime. Gosched() // before printing B, give up the time slice occupied by the current goroutine
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
    }
    wg.Wait()
}

See the above code. Before printing B, let’s give up the time slice occupied by the current goroutine. What will be the output result?

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/tempCodeRunnerFile.go"
A
A
A
A
A
A
A
A
A
A
B
B
B
B
B
B
B
B
B
B
[Done] exited with code=0 in 0.574 seconds

As you can see, print all a before printing B, because the two goroutines opened in each cycle are executed alternately. When the go collaboration of myprintb grabs the time slice, it is executed internallyfmt.Println("B")Before, give up the time slice of the current goroutine and save the current state. When the time slice is snatched again, continue to execute. There may be a little doubt here (why does the B of the current cycle not continue to execute? And what about all the output a executed first?). To solve this question, leave a guess first (after the current time slice is released, it is robbed by the goroutine of the next cycle. If the time of the current cycle is enough (if it does not proceed to the next cycle so quickly, a new goroutine will not be created), it may be executed in the current cycle). Let’s confirm our conjecture. We let the program sleep for 1s in each cycle

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

var wg sync.WaitGroup

func myPrintA() {
    defer wg.Done()
    fmt.Println("A")
}
func myPrintB() {
    defer wg.Done()
    runtime.Gosched()
    fmt.Println("B")
}
func main() {
    runtime.GOMAXPROCS(1)
    for i := 1; i <= 10; i++ {
        wg.Add(2)
        go myPrintA()
        go myPrintB()
        time.Sleep(time.Second)
    }
    wg.Wait()
}

Output result: output a and B every other second. (after B gives up the time slice, he can grab the time slice again and continue to execute the following code, because there is enough time and idle time slice for him, so it will not be robbed by the goroutine created in the next cycle so soon!)

[Running] go run "/Users/codehope/Study/time-to-go/Code/re.channel/cpu_mult_calc.go"
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
A
B
[Done] exited with code=0 in 10.569 seconds

This work adoptsCC agreement, reprint must indicate the author and the link to this article