借鉴文章:

https://www.topgoer.com/

1
2
3
4
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
}

这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。为什么呢?

例子:

当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。

所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了

1
2
3
4
5
func main() {
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
time.Sleep(time.Second)
}

启动多个goroutine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var wg sync.WaitGroup

func hello(i int) {
defer wg.Done() // goroutine结束就登记-1
fmt.Println("Hello Goroutine!", i)
}
func main() {

for i := 0; i < 10; i++ {
wg.Add(1) // 启动一个goroutine就登记+1
go hello(i)
}
wg.Wait() // 等待所有登记的goroutine都结束
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    package main

import (
"fmt"
"time"
)

func main() {
// 合起来写
go func() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
//time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
if i == 3 {
break
}
}
}

在这段程序中 如果 注释 //time.Sleep(time.Second)这个语句:

新 goroutine 中的无限循环运行非常快,因为 time.Sleep 被注释掉了。

新 goroutine 的打印操作占据了 CPU 的大部分时间,导致主 goroutine 很少或者几乎没有机会运行。

Go 的 goroutine 是由 Go 运行时管理的轻量级线程,采用抢占式调度。

在你的代码中,新 goroutine 中的打印操作和循环执行非常快,没有阻塞点(比如 time.Sleep 或 I/O 操作)。

没有阻塞点会让新 goroutine 占据大量的 CPU 时间,主 goroutine 反而无法得到足够的调度时间。

主 goroutine 需要与新 goroutine 竞争 CPU 时间。
因为新 goroutine 的循环过于频繁(没有阻塞),主 goroutine 可能无法及时获得 CPU 调度机会。

Go中的调度函数使用

runtime.Gosched 的作用:
暂停当前 goroutine 的执行。
将 CPU 的使用机会交还给调度器。
调度器会决定下一个应该执行的 goroutine。

GPM是Go语言运行时(runtime)层面的实现,

在你提供的程序中,主 goroutine 是 main 函数中的代码执行流。具体来说,程序一开始会执行 main() 函数内的内容。
然后,主 goroutine 会启动一个新的后台 goroutine,
通过 go 关键字启动的匿名函数来执行,这个新 goroutine 会执行打印 “world” 的任务。

GPM

  1. G (Goroutine):
    表示一个具体的 goroutine,包含要执行的任务信息,比如函数栈、指令等。
    G 是任务的最小单位。

  2. P (Processor):
    是一个逻辑处理器,用于管理 goroutine 的运行队列。
    P 和 CPU 核心的数量绑定(通过 GOMAXPROCS 控制),每个 P 会调度多个 G。

  3. M (Machine):
    表示一个操作系统线程。
    M 从 P 的队列中获取 G 并执行它。
    每个 M 会从 P 上获取一个或多个 G 来执行。

在 Go 的 GPM 调度模型中,P 和 M 之间的关系是 一对多,
即每个 P(处理器)可以关联多个 M(操作系统线程),但同一时刻每个 P 只有一个 M 处于 活跃 状态,
来执行调度的任务(即执行 G,即 goroutine)。

可增长的栈

OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),
goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。
所以在Go语言中一次创建十万左右的goroutine也是可以的。

异步

异步执行的核心思想是让某些任务在后台运行,不阻塞主流程。

go a(ch) 启动 a 函数的执行,但不会阻塞主 goroutine,主 goroutine 可以继续执行后续代码。
a(ch) 在后台运行,它执行完后通过 channel 通知主 goroutine。
主 goroutine 等待 signal,通过 <-ch 实现同步等待,确保 a 完成后才继续后续操作。
因此,go a(ch) 启动的部分是异步的,意味着 a 会在后台执行,而主 goroutine 不会因调用 a 而阻塞或等待它完成。

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。
为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。

channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,
保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型

关闭后的通道有以下特点:

1
2
3
4
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。

无缓冲的通道(同步通道)

无缓冲的通道又称为阻塞的通道

启用一个goroutine去接收值

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main结束")
}