go语言的成神之路-筑基篇-并发
目录
一、go协程
二、GMP
示例:
GMP 的工作流程:
使用 GMP 的好处:
注意事项:
三、runtime包
Gosched
Goexit
调用Goexit之前
调用Goexit之前运行的结果
调用Goexit之后
调用Goexit之后运行的结果
GOMAXPROCS
执行时间不同的原因:
思考:更改参数n的值会使执行时间相同吗?
修改方案
部分代码解析
一、go协程
在 Go 语言中,使用
goroutine
来实现并发。goroutine
是一种轻量级的线程,可以通过go
关键字来启动。
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("hello goroutine")
}
func main() {
go hello()
fmt.Println("main goroutine")
time.Sleep(time.Second)
}
func main() {...}
:这是 Go 程序的入口函数,程序从这里开始执行。go hello()
:使用go
关键字启动一个新的goroutine
来调用hello
函数。goroutine
是 Go 语言中的轻量级线程,它允许并发执行代码。当执行到这一行时,会创建一个新的执行流,该执行流将调用hello
函数,而不会阻塞当前的执行流程。fmt.Println("main goroutine")
:在主goroutine
中打印出"main goroutine"
字符串。time.Sleep(time.Second)
:让主goroutine
暂停执行一秒钟。这是为了确保程序不会立即退出,因为新启动的goroutine
可能还没有机会执行完hello
函数。如果没有这行代码,程序可能会在hello
函数执行之前就结束,因为主goroutine
会直接结束,导致整个程序终止。
package main
import (
"fmt"
"time"
)
func hello(i int) {
fmt.Println("hello goroutine", i)
}
func main() {
for i := 0; i < 10; i++ {
go hello(i)
}
fmt.Println("main goroutine")
time.Sleep(time.Second)
}
仔细观察一下不难看出,这两次输出的结果都不相同,原因如下:
在 Go 语言中,
goroutine
的执行顺序是不确定的。当你使用go
关键字启动多个goroutine
时,它们会被调度到 Go 的运行时环境中并发执行。以下是导致打印顺序不同的原因:
- 调度器的随机性:Go 的调度器会根据系统资源和其他因素来决定
goroutine
的执行顺序。不同的goroutine
可能在不同的系统线程上执行,并且它们的执行顺序取决于调度器的决策,而不是它们被创建的顺序。- 并发执行:由于
goroutine
是并发执行的,它们可能在不同的时间点开始和完成,这取决于系统的负载、CPU 核心的可用性等因素。
思考题
二、GMP
- G (Goroutines):Goroutines 是 Go 语言中的轻量级线程,它们是并发执行的基本单元。可以使用
go
关键字启动一个新的 Goroutine,例如go func() {...}()
。Goroutines 非常轻量,可以轻松创建大量的 Goroutines,并且它们由 Go 的运行时系统管理。 - M (Workers):M 代表工作线程,它们是操作系统线程,由 Go 的运行时系统创建和管理。M 负责执行 Goroutines,一个 M 可以执行多个 Goroutines。
- P (Processors):P 是逻辑处理器,它是一个抽象的概念,代表执行上下文。每个 M 都需要绑定到一个 P 上才能执行 Goroutines。P 维护一个本地的 Goroutine 队列,当一个 Goroutine 被创建时,它会被放入一个 P 的队列中等待执行。
示例:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
// 启动多个 goroutines
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
// 等待所有 goroutines to complete
wg.Wait()
}
var wg sync.WaitGroup
:创建一个sync.WaitGroup
实例,用于等待一组 Goroutines 完成。wg.Add(1)
:在启动每个 Goroutine 之前,调用wg.Add(1)
来增加等待组的计数,表示有一个新的 Goroutine 要等待。go worker(i, &wg)
:启动一个新的 Goroutine 来执行worker
函数,并将wg
的指针传递给它。defer wg.Done()
:在worker
函数中,使用defer wg.Done()
来通知WaitGroup
该 Goroutine 已经完成。wg.Wait()
:在主函数中,调用wg.Wait()
会阻塞,直到WaitGroup
的计数为 0,即所有 Goroutines 都已完成。
GMP 的工作流程:
- 当你启动一个新的 Goroutine 时,它会被放入一个 P 的本地队列中。
- M 从 P 的队列中取出 Goroutines 并执行它们。
- 如果一个 P 的队列是空的,M 可以从全局队列中获取 Goroutines 或者从其他 P 的队列中窃取 Goroutines(work stealing)。
- 当一个 Goroutine 执行
I/O
操作或阻塞时,M 可以切换到另一个 Goroutine 执行,提高并发性能。
使用 GMP 的好处:
- 高效的并发:Go 的 GMP 模型允许高效地创建和管理大量的并发任务,而不会像传统线程那样消耗大量的系统资源。
- 自动调度:Go 的运行时系统自动调度 Goroutines,根据系统资源和任务的需求进行优化,提高性能。
注意事项:
- 避免 Goroutine 泄漏:确保 Goroutines 最终会结束,否则可能会导致资源泄漏。
- 避免过度创建 Goroutines:虽然 Goroutines 很轻量,但创建过多可能会导致性能问题,尤其是在资源有限的系统上。
三、runtime包
Gosched
package main
import (
"fmt"
"runtime"
)
func main() {
// 启动一个匿名 goroutine,接收一个字符串参数 s
go func(s string) {
// 循环两次
for i := 0; i < 2; i++ {
// 打印传入的字符串 s
fmt.Println(s)
}
}("hello")
// 主 goroutine
for i := 0; i < 2; i++ {
// 让出时间片,让其他 goroutine 有机会执行
runtime.Gosched()
// 打印主 goroutine 信息
fmt.Println("main goroutine")
}
}
runtime.Gosched()
:这个函数调用会使当前 goroutine(在这种情况下是主 goroutine)让出处理器,允许其他 goroutine 运行。它放弃自己的时间片并重新进入可运行的 goroutine 队列,以便其他 goroutine 有机会执行。
Goexit
runtime.Goexit()
是 Go 语言runtime
包中的一个函数,它的主要作用是立即终止当前正在执行的goroutine
,而不会影响其他goroutine
的执行。- 当
runtime.Goexit()
被调用时,它会执行当前goroutine
中已注册的defer
函数,然后终止该goroutine
,但不会执行goroutine
中runtime.Goexit()
之后的代码。
调用Goexit之前
调用Goexit之前运行的结果
调用Goexit之后
调用Goexit之后运行的结果
GOMAXPROCS
runtime.GOMAXPROCS(n int)
是 Go 语言runtime
包中的一个函数,它用于设置可以同时执行的最大 CPU 核心数,也就是可以同时运行的goroutine
的最大数量。- 参数
n
表示要使用的 CPU 核心数。如果n
小于 1,则表示不限制 CPU 核心数,Go 运行时会根据系统的 CPU 核心数自动调整。
可以看出当参数为1的时候执行的时间不同 ,那么原因是什么?
首先对代码部分进行简单分析
package main
import (
"fmt"
"runtime"
"time"
)
func a() {
for i := 0; i < 5; i++ {
fmt.Println("A: ", i)
}
fmt.Println(time.Now())
}
func b() {
for i := 0; i < 5; i++ {
fmt.Println("B: ", i)
}
fmt.Println(time.Now())
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
runtime.GOMAXPROCS(1)
:将最大的可同时执行的goroutine
数量设置为 1。这意味着在任何给定的时间,只有一个goroutine
会在 CPU 上执行,其他goroutine
会等待。go a()
和go b()
:启动两个goroutine
,分别执行函数a
和b
。time.Sleep(time.Second)
:让主goroutine
暂停执行一秒钟,以等待a
和b
两个goroutine
有机会执行。
执行时间不同的原因:
- 尽管
runtime.GOMAXPROCS(1)
限制了并发执行的goroutine
数量为 1,但goroutine
的调度顺序仍然是不确定的。 - 当
a
和b
两个goroutine
被创建后,它们会被放入goroutine
队列中等待调度。由于goroutine
的调度是由 Go 的运行时系统决定的,所以a
和b
哪个先被执行是不确定的。 - 即使
a
先被调度,它可能会在执行过程中被暂停,然后b
被调度,或者反之。这取决于 Go 运行时的调度决策,包括当前系统的负载、其他goroutine
的状态等因素。
思考:更改参数n的值会使执行时间相同吗?
runtime.GOMAXPROCS(2)
会允许最多 2 个goroutine
同时运行,但这并不保证a
和b
函数的执行顺序会固定。- 即使有 2 个 CPU 核心可用,Go 的运行时调度器仍然会根据各种因素(如系统负载、其他
goroutine
的状态等)来决定哪个goroutine
先执行,以及它们的执行顺序。 - 每个
goroutine
的执行时间可能会受到系统资源分配、操作系统调度、其他进程的影响,因此即使有更多的 CPU 资源,也不能保证a
和b
的执行顺序和时间是固定的。
修改方案
可以通过管道来同步时间
package main
import (
"fmt"
"time"
)
func a(ch chan bool) {
for i := 0; i < 5; i++ {
fmt.Println("A: ", i)
}
fmt.Println(time.Now())
ch <- true
}
func b(ch chan bool) {
<-ch
for i := 0; i < 5; i++ {
fmt.Println("B: ", i)
}
fmt.Println(time.Now())
}
func main() {
ch := make(chan bool)
go a(ch)
go b(ch)
time.Sleep(time.Second)
}
部分代码解析
ch := make(chan bool)
:创建一个布尔类型的通道。func a(ch chan bool)
:函数a
执行完后会向通道ch
发送一个true
值。func b(ch chan bool)
:函数b
会从通道ch
接收一个值,这会阻塞b
的执行,直到a
发送一个值到通道。这样可以确保b
在a
完成后开始执行。