Initial commit
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
# 0. 序言:Go 的并发哲学
|
||||
|
||||
Go 语言在并发编程领域推崇一个核心哲学:
|
||||
|
||||
> “不要通过共享内存来通信,而应该通过通信来共享内存。”
|
||||
>
|
||||
> "Don't communicate by sharing memory, share memory by communicating."
|
||||
|
||||
这句话的含义是,相比于传统并发模型中使用锁(`sync.Mutex` 等)来保护共享数据(共享内存),Go 更推荐使用 **Channel** 来在 Goroutine 之间传递数据(通信),从而自然地避免数据竞争问题。
|
||||
|
||||
- **精炼说明**: 这并不是说锁在 Go 中是无用的。锁在保护状态(State)时非常重要,而 Channel 在传递数据和同步事件(Communication and Synchronization)时更具优势。选择哪种工具取决于具体场景。
|
||||
|
||||
# 1. Channel 基础
|
||||
|
||||
## 1.1 什么是 Channel?
|
||||
|
||||
Channel(通道)是 Goroutine 之间的通信管道。您可以把它想象成一个传送带,一个 Goroutine 将数据放到传送带的一端,另一个 Goroutine 从另一端取走。这确保了数据的安全传递。
|
||||
|
||||
## 1.2 创建与基本操作
|
||||
|
||||
Channel 是一种引用类型,需要使用 `make` 函数来创建。
|
||||
|
||||
- **声明**: `var ch chan int`
|
||||
- **创建**: `ch = make(chan int)` (创建一个类型为 `int` 的无缓冲通道)
|
||||
- **发送 (存)**: `ch <- 10` (将值 `10` 发送到通道 `ch`)
|
||||
- **接收 (取)**: `data := <-ch` (从通道 `ch` 接收值并赋给 `data`)
|
||||
|
||||
## 1.3 基础使用示例
|
||||
|
||||
**所有 Channel 的操作都必须发生在至少两个或以上的 Goroutine 中**,否则会因阻塞而导致死锁。`main` 函数本身运行在一个主 Goroutine 中。
|
||||
|
||||
下面的代码演示了主 Goroutine 如何等待一个子 Goroutine 完成任务后发出的“完成”信号。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. 创建一个用于通信的 bool 类型通道
|
||||
// make(chan bool) 创建的是一个无缓冲通道
|
||||
done := make(chan bool)
|
||||
|
||||
// 2. 启动一个新的 goroutine 来执行耗时任务
|
||||
go func() {
|
||||
fmt.Println("子 goroutine 开始执行任务...")
|
||||
// 模拟工作
|
||||
for i := 0; i < 5; i++ {
|
||||
fmt.Println("goroutine-", i)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
fmt.Println("子 goroutine 任务完成。")
|
||||
|
||||
// 3. 任务完成后,向通道发送一个信号
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// 4. 主 goroutine 在这里阻塞,等待从 `done` 通道接收信号
|
||||
// 一旦子 goroutine 向 `done` 中写入数据,这里就会解除阻塞
|
||||
<-done
|
||||
|
||||
fmt.Println("主 goroutine 收到完成信号,程序退出。")
|
||||
}
|
||||
```
|
||||
|
||||
# 2. Channel 的核心特性:阻塞
|
||||
|
||||
默认情况下,Channel 的发送和接收操作都是 **阻塞** 的。这是一种强大的同步机制。
|
||||
|
||||
- **发送阻塞**: 当一个 Goroutine 向无缓冲 Channel 发送数据时,它会**阻塞**,直到另一个 Goroutine 从该 Channel 接收数据。
|
||||
- **接收阻塞**: 当一个 Goroutine 尝试从一个空的 Channel 接收数据时,它会**阻塞**,直到另一个 Goroutine 向该 Channel 发送数据。
|
||||
|
||||
这种“成对”的同步特性意味着,每一次通过无缓冲 Channel 的通信,都发生在发送方和接收方都准备好的那一刻。
|
||||
|
||||
## WaitGroup` vs `Channel`
|
||||
|
||||
在等待多个 Goroutine 完成的场景下,除了用 Channel,`sync.WaitGroup` 也是常用工具。
|
||||
|
||||
- **`sync.WaitGroup`**: 主要用于等待一组 Goroutine **全部完成**。它是一个计数器,不关心 Goroutine 的执行结果,只关心“是否做完”。
|
||||
- **`Channel`**: 不仅可以等待 Goroutine 完成,还可以**接收它返回的数据**。功能更强大,但用于纯粹的等待时,代码可能比 `WaitGroup` 稍显繁琐。
|
||||
|
||||
# 3. 常见陷阱:死锁 (Deadlock)
|
||||
|
||||
死锁是指所有 Goroutine 都在互相等待,但谁也无法继续执行,导致程序永久挂起。这是使用 Channel 时最需要警惕的问题。
|
||||
|
||||
## 3.1 什么是死锁?
|
||||
|
||||
当一个 Goroutine 对一个 Channel 进行操作(发送或接收),但**永远没有**其他 Goroutine 来与之配对(接收或发送),就会发生死lock。
|
||||
|
||||
## 3.2 死锁的常见场景
|
||||
|
||||
**场景一:在单 Goroutine 中同时读写**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
// fatal error: all goroutines are asleep - deadlock!
|
||||
func main() {
|
||||
// 创建一个无缓冲通道
|
||||
ch := make(chan int)
|
||||
|
||||
// 错误!main goroutine 在这里发送数据,但被立即阻塞。
|
||||
// 因为没有其他 goroutine 来接收数据,所以这个阻塞永远无法解除。
|
||||
ch <- 10 // 程序在此处死锁
|
||||
|
||||
// 这行代码永远不会执行
|
||||
// <-ch
|
||||
}
|
||||
```
|
||||
|
||||
**场景二:缓冲通道写入超出容量**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// fatal error: all goroutines are asleep - deadlock!
|
||||
func main() {
|
||||
// 创建一个容量为 2 的缓冲通道
|
||||
ch := make(chan int, 2)
|
||||
|
||||
ch <- 1
|
||||
ch <- 2
|
||||
fmt.Println("已成功发送 2 个数据到缓冲通道")
|
||||
|
||||
// 错误!缓冲区已满(容量为 2),main goroutine 尝试再次发送。
|
||||
// 由于没有其他 goroutine 从通道中取出数据,发送操作将被阻塞,导致死锁。
|
||||
ch <- 3 // 程序在此处死锁
|
||||
|
||||
fmt.Println("这行代码永远不会执行到")
|
||||
}
|
||||
```
|
||||
|
||||
**场景三:从空缓冲通道读取**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// fatal error: all goroutines are asleep - deadlock!
|
||||
func main() {
|
||||
ch := make(chan int, 2)
|
||||
ch <- 1
|
||||
|
||||
x := <-ch // 正常,取出数据 1
|
||||
fmt.Printf("成功取出数据: %d\n", x)
|
||||
|
||||
// 错误!通道中已无数据,main goroutine 尝试再次读取。
|
||||
// 由于没有其他 goroutine 往通道中放入数据,读取操作将被阻塞,导致死锁。
|
||||
x = <-ch // 程序在此处死锁
|
||||
|
||||
fmt.Println("这行代码永远不会执行到")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 4. 关闭 Channel
|
||||
|
||||
## 4.1 为何及如何关闭
|
||||
|
||||
`close(ch)` 用于关闭一个 Channel,它向接收方发出一个明确信号:**“通道中不会再有新的数据发送过来了”**。
|
||||
|
||||
- **关闭原则:**
|
||||
1. **由发送方关闭**:永远不要让接收方关闭 Channel,因为接收方无法知道发送方是否还会发送数据。让发送方在确定所有数据都已发送完毕后关闭 Channel,是最安全的实践。
|
||||
2. 关闭已关闭的 Channel 会引发 `panic`。
|
||||
3. 向已关闭的 Channel 发送数据会引发 `panic`。
|
||||
4. 从已关闭的 Channel 接收数据不会阻塞。会立即返回该 Channel 类型的零值。
|
||||
|
||||
## 4.2 安全地接收数据
|
||||
|
||||
当接收方不确定 Channel 是否关闭时,可以使用多重返回值来判断。
|
||||
|
||||
`data, ok := <-ch`
|
||||
|
||||
- `ok` 为 `true`:表示成功从 Channel 中读取到数据 `data`。
|
||||
- `ok` 为 `false`:表示 Channel 已被关闭,且其中已无数据可读。此时 `data` 是该 Channel 类型的零值(如 `int` 是 `0`,`string` 是 `""`)。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// producer 函数负责生产数据并发送到通道,完成后关闭通道
|
||||
func producer(ch chan int) {
|
||||
for i := 0; i < 5; i++ {
|
||||
fmt.Printf("发送数据: %d\n", i)
|
||||
ch <- i
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
fmt.Println("所有数据已发送完毕,关闭通道。")
|
||||
close(ch) // 发送方在发送完所有数据后关闭通道
|
||||
}
|
||||
|
||||
func main() {
|
||||
ch1 := make(chan int)
|
||||
go producer(ch1)
|
||||
|
||||
// 使用 for 循环和 ok 判断来消费数据
|
||||
for {
|
||||
// ok 模式可以安全地判断通道是否已关闭
|
||||
data, ok := <-ch1
|
||||
if !ok {
|
||||
fmt.Println("通道已关闭,读取结束。")
|
||||
break // 退出循环
|
||||
}
|
||||
fmt.Printf("接收到数据: %d\n", data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4.3 使用 `for range` 遍历 Channel
|
||||
|
||||
使用 `for range` 循环可以更优雅地从 Channel 中读取数据。该循环会自动监听 Channel,直到 Channel 被关闭并取完所有值后,循环会自动结束。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// test7 函数(保持原名)生产数据并发送,完成后关闭通道
|
||||
func test7(ch chan int) {
|
||||
for i := 0; i < 10; i++ {
|
||||
ch <- i
|
||||
}
|
||||
// 发送方负责关闭
|
||||
close(ch)
|
||||
}
|
||||
|
||||
func main() {
|
||||
ch1 := make(chan int)
|
||||
go test7(ch1)
|
||||
|
||||
// for range 会自动处理通道的关闭状态。
|
||||
// 当通道 ch1 被关闭且缓冲区为空时,循环会自动退出。
|
||||
// 这比使用 `data, ok := <-ch1` 的无限循环更简洁。
|
||||
fmt.Println("开始使用 for range 接收数据...")
|
||||
for data := range ch1 {
|
||||
fmt.Printf("接收到: %d\n", data)
|
||||
time.Sleep(200 * time.Millisecond) // 模拟消费耗时
|
||||
}
|
||||
|
||||
fmt.Println("接收循环结束,主程序退出。")
|
||||
}
|
||||
```
|
||||
|
||||
# 5. 缓冲 Channel (Buffered Channel)
|
||||
|
||||
## 5.1 定义与特性
|
||||
|
||||
在创建 Channel 时,可以指定一个容量,这就是缓冲 Channel。
|
||||
|
||||
`ch := make(chan int, 3)` // 创建一个容量为 3 的 int 类型缓冲通道
|
||||
|
||||
- **发送**: 向缓冲 Channel 发送数据时,只有在**缓冲区已满**的情况下才会阻塞。
|
||||
- **接收**: 从缓冲 Channel 接收数据时,只有在**缓冲区为空**的情况下才会阻塞。
|
||||
|
||||
缓冲 Channel 是一种**异步**通信方式,可以减少 Goroutine 间因同步阻塞而导致的等待,有助于提升性能,尤其是在生产者和消费者速度不匹配的场景下。
|
||||
|
||||
## 5.2 正确使用示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建一个容量为 3 的缓冲通道
|
||||
// 这意味着发送方可以连续发送 3 个值而不会被阻塞
|
||||
ch := make(chan int, 3)
|
||||
|
||||
go func() {
|
||||
defer close(ch) // 确保 goroutine 退出时关闭通道
|
||||
for i := 1; i <= 5; i++ {
|
||||
fmt.Printf("发送方: 准备发送 %d\n", i)
|
||||
ch <- i
|
||||
fmt.Printf("发送方: 已发送 %d\n", i)
|
||||
}
|
||||
fmt.Println("发送方: 所有数据发送完毕。")
|
||||
}()
|
||||
|
||||
// 主 goroutine (接收方) 稍等一下,让发送方先填满缓冲区
|
||||
fmt.Println("接收方: 等待 2 秒...")
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
fmt.Println("接收方: 开始接收数据...")
|
||||
// 使用 for range 消费通道中的所有数据
|
||||
for val := range ch {
|
||||
fmt.Printf("接收方: 收到 %d\n", val)
|
||||
// 模拟消费数据的耗时
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
fmt.Println("接收方: 通道已关闭,所有数据接收完毕。")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 6. 高级用法
|
||||
|
||||
## 6.1 定向 Channel (Directional Channel)
|
||||
|
||||
为了增强代码的类型安全和可读性,可以将 Channel 限制为**只发送**或**只接收**。这在函数签名中特别有用,可以明确规定函数对 Channel 的操作权限。
|
||||
|
||||
- **只发送通道**: `chan<- int`
|
||||
- **只接收通道**: `<-chan int`
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// writeOnly 函数被限制为只能向通道中写入数据
|
||||
// 参数类型 `chan<- int` 明确了其职责,防止误操作
|
||||
func writeOnly(ch chan<- int) {
|
||||
fmt.Println("开始写入数据...")
|
||||
for i := 0; i < 3; i++ {
|
||||
val := (i + 1) * 100
|
||||
ch <- val
|
||||
fmt.Printf("写入: %d\n", val)
|
||||
}
|
||||
close(ch) // 写入者负责关闭
|
||||
}
|
||||
|
||||
// readOnly 函数被限制为只能从通道中读取数据
|
||||
// 参数类型 `<-chan int` 保证了函数内部不会污染通道
|
||||
func readOnly(ch <-chan int) {
|
||||
fmt.Println("开始读取数据...")
|
||||
for data := range ch {
|
||||
fmt.Printf("读取到: %d\n", data)
|
||||
}
|
||||
fmt.Println("读取完毕。")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 创建一个双向通道
|
||||
ch1 := make(chan int)
|
||||
|
||||
// 当 ch1 传递给函数时,会自动转换为指定的单向类型
|
||||
go writeOnly(ch1)
|
||||
go readOnly(ch1)
|
||||
|
||||
// 等待两个 goroutine 执行完毕
|
||||
// 这里的等待逻辑可以更健壮,例如使用 WaitGroup
|
||||
time.Sleep(1 * time.Second)
|
||||
fmt.Println("程序执行完毕。")
|
||||
}
|
||||
```
|
||||
|
||||
## 6.2 `select` 语句
|
||||
|
||||
`select` 语句让一个 Goroutine 可以**同时等待多个 Channel 操作**,其行为类似于 `switch` 语句,但每个 `case` 必须是一个 Channel 操作。
|
||||
|
||||
- **规则 1**: `select` 会阻塞,直到其中一个 `case` 的 Channel 操作就绪(即可读或可写)。
|
||||
- **规则 2**: 如果有多个 `case` 同时就绪,`select` 会**随机选择一个**执行。
|
||||
- **规则 3**: default` 子句:如果 `select` 中包含 `default`,它会使 `select` 变为**非阻塞**的。如果没有任何 `case` 就绪,`default` 会被立即执行。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ch1 := make(chan string)
|
||||
ch2 := make(chan string)
|
||||
|
||||
// Goroutine 1: 2秒后向 ch1 发送消息
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
ch1 <- "来自 ch1 的消息"
|
||||
}()
|
||||
|
||||
// Goroutine 2: 1秒后向 ch2 发送消息
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
ch2 <- "来自 ch2 的消息"
|
||||
}()
|
||||
|
||||
fmt.Println("等待消息...")
|
||||
|
||||
// select 会等待 ch1 或 ch2 中任意一个先准备好
|
||||
// 在本例中,ch2 会在 1 秒后先就绪
|
||||
select {
|
||||
case msg1 := <-ch1:
|
||||
fmt.Println("收到了:", msg1)
|
||||
case msg2 := <-ch2:
|
||||
fmt.Println("收到了:", msg2) // 这条 case 会被执行
|
||||
}
|
||||
|
||||
fmt.Println("第一次 select 结束。")
|
||||
|
||||
// [补充示例] 带 default 的非阻塞 select
|
||||
// 此时 ch1 和 ch2 都为空,也没有 goroutine 会向它们发送数据
|
||||
select {
|
||||
case msg1 := <-ch1:
|
||||
fmt.Println("第二次收到了:", msg1)
|
||||
case msg2 := <-ch2:
|
||||
fmt.Println("第二次收到了:", msg2)
|
||||
default:
|
||||
// 因为 ch1 和 ch2 都不可读,所以会立即执行 default
|
||||
fmt.Println("没有任何消息可以立即接收。")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 7. 应用场景:定时器
|
||||
|
||||
`time` 包中的定时器功能是 Channel 的一个极佳应用范例。
|
||||
|
||||
- **`time.NewTimer`**: 创建一个定时器对象,在指定时间后,会将当前时间发送到其内部的 `C` Channel 中。可以调用 `Stop()` 来取消。
|
||||
- **`time.After`**: 一个更简洁的用法,返回一个 Channel。在指定时间后,该 Channel 会接收到一个时间值。它像一个一次性的闹钟。
|
||||
- **`time.AfterFunc`**: 在指定时间后,直接执行一个函数,不使用 Channel。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func sendEmail() {
|
||||
fmt.Println("定时任务触发:正在发送邮件...")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 示例 1: 使用 time.After 等待一个事件
|
||||
fmt.Println("程序启动时间:", time.Now().Format("15:04:05"))
|
||||
fmt.Println("设置一个 2 秒后触发的事件...")
|
||||
|
||||
// time.After 返回一个 <-chan Time 类型的通道
|
||||
// main goroutine 会阻塞在这里,直到 2 秒后通道接收到值
|
||||
timerChan := time.After(2 * time.Second)
|
||||
<-timerChan
|
||||
|
||||
fmt.Println("2 秒已到,事件触发!当前时间:", time.Now().Format("15:04:05"))
|
||||
|
||||
// 示例 2: 使用 time.AfterFunc 安排一个未来的回调函数
|
||||
// 这是一种 "fire and forget" 的方式,不会阻塞当前 goroutine
|
||||
fmt.Println("\n设置一个 3 秒后执行的邮件发送任务...")
|
||||
time.AfterFunc(3*time.Second, sendEmail)
|
||||
|
||||
// 主程序需要等待足够长的时间,以确保 AfterFunc 的任务有机会执行
|
||||
// 在实际应用中,主程序通常有自己的主循环,不会这样简单地退出
|
||||
fmt.Println("主程序继续执行其他任务...")
|
||||
time.Sleep(4 * time.Second) // 等待超过 3 秒
|
||||
|
||||
fmt.Println("\n主程序退出。")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 8. 拓展学习
|
||||
|
||||
- **Go 并发模型 (GPM)**: 要深入理解 Goroutine 是如何被 Go 运行时调度的,可以学习 GPM(Goroutine, Processor, Machine)模型。您提供的链接是一个很好的起点:[GPM模型讲解](https://www.bilibili.com/video/BV1hv411x7we?p=16)
|
||||
Reference in New Issue
Block a user