文章目录
一、前言
今天玩点不一样的,我想试试看 GPT-4 能不能当好一个编程老师,循循善诱,由浅入深,深入浅出…… 给我讲解下 Golang 里的并发机制。
今天倒掉腹中半杯水,当回白纸,GPT 为笔,代码为颜料,挥笔泼墨,且看能绘出怎样的世界。
二、开聊
下文通过 DevChat 和 GPT-4 交互,如果你对 DevChat 感兴趣,可以跳转下文浏览:
2.1 Golang 里的并发机制介绍
- 在 DevChat 里和 GPT-4 交互的体验:
下文主要贴文字,避免过多使用截图。
- 我问 GPT:
能不能介绍下 Golang 里的并发机制
- GPT 说:
在 Golang 中,主要的并发机制是通过 Goroutines 和 Channels 实现的。
Goroutines:Goroutines 是 Go 语言中的轻量级线程实现,由 Go 运行时环境管理。Goroutines 相比于线程的优势在于其创建和销毁的开销小,可以轻松创建上百万个而不会导致系统资源耗尽。创建一个 Goroutine 只需要在函数调用前加上关键字
go
即可。Channels:Channels 是 Go 中提供的一种强大的并发同步机制。它可以在多个 Goroutine 之间传递数据并同步执行。你可以把它想象成一个 Goroutines 可以发送和接收值的管道。通过
make
函数创建 Channels,通过<-
操作符发送和接收数据。以下是一个简单的示例,展示了如何在 Go 中使用 Goroutines 和 Channels:
package main import ( "fmt" "time" ) func say(s string, c chan string) { for i := 0; i < 5; i++ { time.Sleep(1 * time.Second) c <- s } close(c) } func main() { c := make(chan string) go say("Hello", c) for msg := range c { fmt.Println(msg) } }
在这个示例中,我们创建了一个 Goroutine
say
,它每秒向 Channelc
发送一次 “Hello”,然后在 main 函数中,我们从 Channelc
中接收并打印这些消息。当 Channelc
被关闭并且没有更多的值可以接收时,range c
循环将结束。
GPT 给了一段示例代码,并且在最后给出了这段代码的解释,整体看来还是挺通俗易懂。接下来我想让 GPT 解释下 Goroutine 和“线程”的关系。我大概记得 Goroutine 是用户级线程,Go 的 runtime 自己管理和调度的,或者简单地叫做“协程”;而线程在这里指的是内核级线程,OS 完成的管理和调度。且看下 GPT 怎么说吧。
2.2 Goroutine 与线程
- 我问 GPT:
Goroutines 和线程的关系是什么?Goroutines 调度和线程调度分别是什么原理?
- GPT 说:
Goroutines 和线程的关系:
- Goroutines 是 Go 语言中的并发体,它是一种轻量级的线程,由 Go 运行时(runtime)管理和调度,而不是由操作系统直接管理。
- Goroutines 相比于操作系统的线程,其创建和销毁的开销更小,内存占用更少,可以轻松创建和管理上百万个 Goroutines。
- 一个 Go 程序在运行时会有多个系统线程,每个系统线程在运行多个 Goroutine。Go 运行时会负责在这些系统线程上调度 Goroutines 的执行。
Goroutines 的调度:
- Go 语言运行时包含了自己的调度器,这个调度器使用了 M:N 的调度模型,即 M 个 Goroutines 映射到 N 个系统线程上。
- Go 调度器的工作方式和操作系统的调度器类似,但是在用户态完成调度,不需要切换到内核态,因此调度开销小。
- Go 调度器使用了一种叫做分段式的抢却式调度策略,当一个 Goroutine 在执行时,会定期检查是否需要进行调度(例如,是否有更高优先级的 Goroutine 等待运行),这样可以保证 Goroutines 的公平调度。
线程的调度:
- 线程的调度是由操作系统完成的,通常使用一种叫做抢占式的调度策略,当一个线程在执行时,操作系统会定期中断它,保存它的状态,然后选择另一个线程运行。
- 线程的创建、销毁和切换都需要在内核态完成,因此开销比 Goroutines 大。
- 线程可以利用多核处理器的并行计算能力,但是过多的线程会导致调度开销增大,影响程序的性能。
2.3 Goroutine 与线程的调度开销
- 我问 GPT:
为什么线程调度的开销更大?
- GPT 说:
线程调度的开销主要来自以下几个方面:
上下文切换:当操作系统决定停止运行一个线程并运行另一个线程时,它需要保存当前线程的状态(包括程序计数器,寄存器,内存映射等),然后加载另一个线程的状态。这个过程叫做上下文切换,它需要在用户态和内核态之间切换,这是一个相对耗时的操作。
内核态和用户态的切换:线程是由操作系统管理的,所以线程的创建、销毁和调度都需要在内核态完成。每次从用户态切换到内核态,都需要保存和恢复大量的状态信息,这也会增加额外的