Go天生的并发优势
借鉴文章:
1 | func main() { |
这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。为什么呢?
例子:
当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。
所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了
1 | func main() { |
启动多个goroutine
1 | var wg sync.WaitGroup |
1 | package main |
在这段程序中 如果 注释 //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
G (Goroutine):
表示一个具体的 goroutine,包含要执行的任务信息,比如函数栈、指令等。
G 是任务的最小单位。P (Processor):
是一个逻辑处理器,用于管理 goroutine 的运行队列。
P 和 CPU 核心的数量绑定(通过 GOMAXPROCS 控制),每个 P 会调度多个 G。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 | 1.对一个关闭的通道再发送值就会导致panic。 |
无缓冲的通道(同步通道)
无缓冲的通道又称为阻塞的通道
启用一个goroutine去接收值
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)
1 | package main |