# 跟着 GPT-4 从0到1学习 Golang 并发机制(三)


## 1. 前言

话接上回[《跟着 GPT-4 从0到1学习 Golang 并发机制(二)》](https://www.danielhu.cn/from_0_to_1_go_concurrency_with_gpt_2/)，我想试试看 GPT-4 能不能当好一个编程老师，循循善诱，由浅入深，深入浅出…… 给我讲解下 Golang 里的并发机制。

今天继续当白纸，GPT 为笔，代码为颜料，挥笔泼墨，接着作画。

## 2. 开聊

> 下文通过 DevChat 和 GPT-4 交互，如果你对 DevChat 感兴趣，可以跳转下文浏览：
> - [《DevChat：将 GPT-4 无缝融入 VS Code，极致提升你的编程体验》](https://zh.devchat.blog/devchat-gpt4-vscode)

### 2.1 关于 goroutine 泄露问题

- **我问 GPT：**

> 可以介绍下 goroutine 泄露问题吗

- **GPT 说：**

> Goroutine 泄露是 Go 程序中一个常见的问题，它发生在你启动了一个 goroutine，但是没有提供一个结束它的方式。这个 goroutine 就会一直运行，占用内存和 CPU，直到整个程序结束。如果你的程序启动了大量的这样的 goroutine，它可能会消耗大量的资源，甚至导致程序崩溃。
> 
> 以下是一个 goroutine 泄露的示例：
> 
> ```go
> package main
> 
> import (
> 	"fmt"
> 	"time"
> )
> 
> func leakyFunction() {
> 	go func() {
> 		for {
> 			fmt.Println("I'm leaking!")
> 			time.Sleep(1 * time.Second)
> 		}
> 	}()
> }
> 
> func main() {
> 	leakyFunction()
> 	time.Sleep(5 * time.Second)
> }
> ```
> 
> 在这个示例中，`leakyFunction` 启动了一个 goroutine，这个 goroutine 会无限循环打印一条消息。然而，`leakyFunction` 并没有提供一个结束这个 goroutine 的方式，所以这个 goroutine 会一直运行，直到整个程序结束。
> 
> 为了避免 goroutine 泄露，你应该总是提供一个结束 goroutine 的方式。这通常可以通过 context 取消、关闭 channel 或者使用 `sync.WaitGroup` 来实现。
> 
> 以下是一个修改后的示例，避免了 goroutine 泄露：
> 
> ```go
> package main
> 
> import (
> 	"context"
> 	"fmt"
> 	"time"
> )
> 
> func notLeakyFunction(ctx context.Context) {
> 	go func() {
> 		for {
> 			select {
> 			case <-ctx.Done():
> 				return
> 			default:
> 				fmt.Println("I'm not leaking!")
> 				time.Sleep(1 * time.Second)
> 			}
> 		}
> 	}()
> }
> 
> func main() {
> 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
> 	defer cancel()
> 	notLeakyFunction(ctx)
> 	time.Sleep(10 * time.Second)
> }
> ```
> 
> 在这个示例中，我们使用了一个可取消的 context。当 context 被取消时，goroutine 会检测到这个信号，并结束运行。这样，我们就避免了 goroutine 泄露。

寥寥几句，似乎已经讲完了这个话题。好吧，那继续看内存模型去。

### 2.2 内存模型

- **我又问 GPT：**

> 继续介绍下 Go 的内存模型吧

- **GPT 说：**

> Go 的内存模型定义了在并发程序中，如何以及何时对变量的读写操作对其他 goroutine 可见。它是理解并发编程中数据同步的关键。
> 
> 在 Go 中，如果一个 goroutine 在没有使用 channel 或者其他同步原语（如 `sync` 包中的锁）的情况下，对一个变量的写入操作对其他 goroutine 是不可见的。这是因为 Go 运行时并不保证在没有同步的情况下，一个 goroutine 的写入操作何时对其他 goroutine 可见。
> 
> 以下是一个示例：
> 
> ```go
> package main
> 
> import (
> 	"fmt"
> 	"time"
> )
> 
> var a string
> var done bool
> 
> func setup() {
> 	a = "hello, world"
> 	done = true
> }
> 
> func main() {
> 	go setup()
> 	time.Sleep(1 * time.Second)
> 	if done {
> 		fmt.Println(a)
> 	}
> }
> ```
> 
> 在这个示例中，`setup` goroutine 对 `a` 和 `done` 的写入操作可能对 `main` goroutine 不可见，即使我们在 `setup` 和 `main` 之间添加了 `time.Sleep`。这是因为 Go 的内存模型并不保证在没有同步的情况下，一个 goroutine 的写入操作何时对其他 goroutine 可见。
> 
> 为了在 goroutine 之间正确同步数据，你应该使用 channel 或者 `sync` 包中的原语。例如，你可以使用 `sync.Mutex` 来保护对共享变量的访问，或者使用 `sync.WaitGroup` 来等待一个 goroutine 完成。
> 
> 以下是一个修改后的示例，使用了 `sync` 包来同步数据：
> 
> ```go
> package main
> 
> import (
> 	"fmt"
> 	"sync"
> )
> 
> var a string
> var done bool
> var mu sync.Mutex
> 
> func setup() {
> 	a = "hello, world"
> 	mu.Lock()
> 	done = true
> 	mu.Unlock()
> }
> 
> func main() {
> 	go setup()
> 	time.Sleep(1 * time.Second)
> 	mu.Lock()
> 	if done {
> 		fmt.Println(a)
> 	}
> 	mu.Unlock()
> }
> ```
> 
> 在这个示例中，我们使用了 `sync.Mutex` 来保护对 `done` 的访问。这样，我们就可以确保 `setup` goroutine 对 `a` 和 `done` 的写入操作对 `main` goroutine 可见。

这个结果看起来并不好，我不满意。

- **我接着问 GPT：**

> 这个例子其实并不能太好地解释“内存模型”，因为不管执行多少次，main 函数中都会读到 done 为 true。我希望你更深入地介绍 happens before 以及 Golang 哲学中的“Do not communicate by sharing memory; instead, share memory by communicating”

- **GPT 说：**

> 你的观察是正确的，我之前的例子可能并不能很好地展示 Go 的内存模型。让我们更深入地讨论一下。
> 
> **Happens Before**
> 
> 在 Go 的内存模型中，"happens before" 是一个非常重要的概念。如果我们说事件 A happens before 事件 B，那么意味着在所有的 goroutine 中，事件 A 的效果对事件 B 是可见的。也就是说，如果一个 goroutine 对一个变量的写入操作 happens before 另一个 goroutine 的读取操作，那么读取操作将会看到写入操作的效果。
> 
> 在 Go 中，以下几种情况会建立 happens before 关系：
> 
> - 对同一个 channel 的发送操作 happens before 相应的接收操作。
> - 对一个 Mutex 或者 RWMutex 的解锁操作 happens before 后续的锁定操作。
> - 对一个 WaitGroup 的 Add 操作 happens before 相应的 Wait 操作。
> - 对一个 Cond 的 Signal 或者 Broadcast 操作 happens before 相应的 Wait 操作返回。
> 
> **"Do not communicate by sharing memory; instead, share memory by communicating"**
> 
> 这是 Go 并发哲学的核心原则。它鼓励我们使用 channel 来在 goroutine 之间传递数据，而不是通过共享内存。这样，我们可以通过数据的所有权转移来避免数据竞争。
> 
> 以下是一个使用 channel 来在 goroutine 之间传递数据的示例：
> 
> ```go
> package main
> 
> import (
> 	"fmt"
> 	"time"
> )
> 
> func worker(done chan bool) {
> 	fmt.Print("working...")
> 	time.Sleep(time.Second)
> 	fmt.Println("done")
> 
> 	done <- true
> }
> 
> func main() {
> 	done := make(chan bool, 1)
> 	go worker(done)
> 
> 	<-done
> }
> ```
> 
> 在这个示例中，我们创建了一个 worker goroutine，并给它传递了一个 channel。当 worker 完成工作后，它会向这个 channel 发送一个信号。主 goroutine 会等待这个信号，然后结束程序。这样，我们就通过 channel 在 goroutine 之间传递了数据，而不是通过共享内存。

- **我接着问 GPT：**

> 看了这个新的例子，我大概理解“happens before”以及“share memory by communicating”了。在前面一个例子中，你通过共享内存来实现 goroutine 之间的通信，其实是不可靠的，因为读取操作可以发生在写入操作之前。如果你没有加上1秒钟的等待，那么完全可能发生读取到的 done 为 false；而通过 channel 通信来实现共享内存就避免了这个问题，因为 done 的读取会等待写入完成。总之这里的关键就是尽量用 channel 实现 goroutine 之间的通信，避免使用共享内存；同时在并发编程时尽量使用 sync 包提供的各种同步锁和条件变量等等机制，避免使用类似“time.Sleep()”这种不可靠的方式去实现 goroutine 同步。我的理解对吗？

- **GPT 说：**

> 你的理解是完全正确的。在 Go 中，我们应该优先使用 channel 来在 goroutine 之间传递数据，而不是通过共享内存。这样可以避免数据竞争，使得代码更容易理解和维护。
> 
> 同时，你也正确地指出了在并发编程中，我们应该使用 `sync` 包提供的同步原语，如 Mutex、WaitGroup 和 Cond，来同步 goroutine 的执行。这些同步原语可以帮助我们建立明确的 happens-before 关系，确保我们的程序按照预期的顺序执行。
> 
> 最后，你也正确地指出了，我们应该避免使用 `time.Sleep()` 这样的方式来同步 goroutine。因为 `time.Sleep()` 并不能保证 happens-before 关系，所以它不能确保 goroutine 的执行顺序。在实际的并发程序中，我们应该使用 channel、Mutex、WaitGroup 或者其他同步原语来同步 goroutine，而不是依赖于 `time.Sleep()`。

### 2.3 Race Detector 检测数据竞争

- **我问 GPT：**

> 最后，我还想知道 Race Detector 是什么

- **GPT 说：**

> Go 语言提供了一个名为 Race Detector 的工具，用于检测代码中的数据竞争条件。数据竞争发生在当两个或更多的 goroutine 并发访问同一块内存区域，且至少有一个访问是写入操作时。
> 
> Race Detector 可以在运行时检测到这些数据竞争，帮助你找到并修复它们。你可以通过在 `go run`、`go test` 或 `go build` 命令后添加 `-race` 选项来启用 Race Detector。
> 
> 以下是一个有数据竞争的程序：
> 
> ```go
> package main
> 
> import (
> 	"fmt"
> )
> 
> var counter int
> 
> func increment() {
> 	counter++
> }
> 
> func main() {
> 	go increment()
> 	go increment()
> 	fmt.Println(counter)
> }
> ```
> 
> 在这个程序中，两个 goroutine 都在尝试增加 `counter` 的值。这会导致数据竞争，因为 `counter++` 不是一个原子操作。
> 
> 如果你使用 Race Detector 来运行这个程序（`go run -race main.go`），它会报告数据竞争，并给出详细的报告，包括数据竞争发生的位置和涉及的 goroutine。
> 
> 注意，虽然 Race Detector 是一个非常有用的工具，但它可能会使你的程序运行得更慢，并使用更多的内存。因此，你应该只在测试和调试时启用它，而不是在生产环境中使用。

看起来挺有道理的，我试下这段代码：

```bash
$ go run -race main.go
0
==================
WARNING: DATA RACE
Write at 0x00010269a8a0 by goroutine 7:
  main.increment()
      /Users/danielhu/go/src/demo/main.go:10 +0x3c

Previous read at 0x00010269a8a0 by main goroutine:
  main.main()
      /Users/danielhu/go/src/demo/main.go:16 +0x44

Goroutine 7 (running) created at:
  main.main()
      /Users/danielhu/go/src/demo/main.go:15 +0x34
==================
Found 1 data race(s)
exit status 66
```

到这里，我还想追问一句：

- **我问 GPT：**

> 所以 Race Detector 其实并不是为了检测并发访问一段数据，而是检测哪些并发访问数据时没有正确地使用合适的同步原语对吗？

- **GPT 说：**

> 是的，你的理解是正确的。Race Detector 的主要目的是检测数据竞争，也就是多个 goroutine 同时访问同一块数据，且至少有一个 goroutine 在进行写操作，而这些操作之间没有进行适当的同步。
> 
> 如果你的代码中存在数据竞争，那么你的代码的行为就是未定义的，可能会导致各种难以预料和难以调试的问题。通过使用 Race Detector，你可以在开发和测试阶段就发现这些问题，然后通过添加适当的同步原语（如互斥锁、channel 等）来修复它们。
> 
> 所以，Race Detector 并不是用来检测并发访问数据本身，而是用来检测并发访问数据时是否存在数据竞争，即是否存在没有进行适当同步的并发读写操作。

## 3. 总结

三篇《跟着 GPT-4 从0到1学习 Golang 并发机制》文章到此写完。温故知新，收获颇丰，随便总结下：

1. **Goroutine**：Goroutine 是 Go 语言的并发单元，属于轻量级的线程，由 Go runtime 管理，也可称协程。
2. **Channel**：Channel 是 Go 语言中在 goroutines 之间通信的主要方式。
3. **Select**：`select` 语句可以用于在多个 channel 操作之间进行选择。当多个 case 都可以运行时，`select` 会随机选择一个。
4. **Mutexes 和 sync 包**：`sync` 包提供了一些同步原语，如 Mutex（互斥锁）、RWMutex（读写锁）、WaitGroup（等待组）和 Cond（条件变量）。这些原语可以用于保护共享数据，或者同步 goroutines 的执行。
5. **原子操作**：Go 语言的 sync/atomic 包提供了一些原子操作函数，这些函数可以在多个 goroutine 之间安全地操作数据，而不需要使用互斥锁。原子操作包括 AddInt32、AddInt64、CompareAndSwapInt32、CompareAndSwapInt64 等等。这些函数可以保证在任何时刻只有一个 goroutine 能够对数据进行操作，从而避免数据竞争。
6. **Context 包**：`context` 包提供了一种在 API 边界之间传递请求范围的值、取消信号和超时信息的方式。你可以使用 `context.WithCancel`、`context.WithDeadline`、`context.WithTimeout` 和 `context.WithValue` 函数来创建新的 Context。
7. **内存模型**：Go 的内存模型定义了在并发程序中，如何以及何时对变量的读写操作对其他 goroutine 可见。你应该使用 channel 或者 `sync` 包中的原语来同步数据，以确保 happens-before 关系。
8. **Race Detector**：Race Detector 是一个用于检测数据竞争的工具。你可以在 `go run`、`go test` 或 `go build` 命令后添加 `-race` 选项来启用 Race Detector。
9. **Go 并发哲学**："Do not communicate by sharing memory; instead, share memory by communicating." 这是 Go 并发哲学的核心原则。它鼓励我们使用 channel 来在 goroutine 之间传递数据，而不是通过共享内存。这样可以避免数据竞争，使得代码更容易理解和维护。

**Do not communicate by sharing memory; instead, share memory by communicating.**

**Do not communicate by sharing memory; instead, share memory by communicating.**

**Do not communicate by sharing memory; instead, share memory by communicating.**

**相关文章**

- [《跟着 GPT-4 从0到1学习 Golang 并发机制(一)》](https://www.danielhu.cn/from_0_to_1_go_concurrency_with_gpt_1/)
- [《跟着 GPT-4 从0到1学习 Golang 并发机制(二)》](https://www.danielhu.cn/from_0_to_1_go_concurrency_with_gpt_2/)

（关注我的个人公众号“胡说云原生”吧）

