Files
notes/resource/go/Go 语言的 Groutine.md
2026-03-01 01:43:46 +08:00

352 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 一、核心概念:进程、线程与协程
- **进程 (Process)**
- **定义**: 程序的一次执行过程,是操作系统进行**资源分配**的基本单位。
- **特点**: 拥有独立的内存空间,创建和切换开销巨大,资源占用多。
- **线程 (Thread)**
- **定义**: 进程内的执行单元,是 **CPU 调度**的基本单位。一个进程至少包含一个主线程。
- **特点**: 共享进程的内存空间,创建和切换开销相比进程小,但仍有相当成本。多线程共享数据时需要处理同步问题(如锁)。
- **协程 (Coroutine / Goroutine)**
- **定义**: Go 语言实现的特有并发体,是一种**用户态的、极其轻量级的线程**。
- **特点**:
- **开销极小**: 创建成本远低于线程,内存占用仅为KB级别(初始约2KB),可以轻松创建数十万甚至上百万个。
- **调度灵活**: 由 Go 语言的运行时(Runtime)进行调度,而非操作系统。调度器能够在 I/O 操作等场景下自动切换,实现高效的并发。
- **非抢占式**: 协程出让 CPU 控制权通常发生在特定的时机(如Channel操作、系统调用、函数调用等),而不是由操作系统强制中断。
## 对比总结
| 特性 | 进程 (Process) | 线程 (Thread) | **协程 (Goroutine)** |
| :------- | :------------- | :----------------- | :------------------------ |
| **角色** | 资源分配单位 | CPU 调度单位 | **Go语言的并发执行体** |
| **开销** | 巨大 | 较大 | **极小** |
| **数量级** | 百级 | 万级 | **百万级** |
| **调度方** | 操作系统 | 操作系统 | **Go 运行时 (Runtime)** |
| **内存** | 独立 | 共享进程内存 | **共享进程内存** |
---
# 二、GoroutineGo 并发编程的基石
Go 语言通过 `go` 关键字来创建 Goroutine,实现并发执行。一个 Goroutine 就是一个与其他函数或方法同时运行的函数。
## 1. 启动一个 Goroutine
语法非常简单,在函数或方法调用前加上 `go` 关键字即可。
```go
go myFunction() // 启动一个新的 Goroutine 来执行 myFunction
```
## 2. Goroutine 的执行特性
1. **立即返回**: `go` 语句会立即返回,不会等待新的 Goroutine 执行完毕。
2. **主 Goroutine 退出**: `main` 函数本身运行在一个特殊的 Goroutine 中,称为主 Goroutine。**如果主 Goroutine 退出,整个程序就会结束,所有其他正在运行的 Goroutine 也会被强制终止。**
### 示例:主 Goroutine 提前退出
下面的代码中,`hello()` 可能根本不会打印任何内容,因为主 Goroutine 在它开始执行前就已经结束了。
```go
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello from new Goroutine!")
}
func main() {
// 启动一个新的 Goroutine
go hello()
fmt.Println("main function finished.")
// 如果没有下面的休眠,程序会立刻退出,"Hello from new Goroutine!" 很可能没有机会打印
// 这是一种不推荐的做法,仅用于演示。正确的方式是使用 sync.WaitGroup。
time.Sleep(1 * time.Second)
}
```
## 3. `sync.WaitGroup`:等待 Goroutine 完成
为了解决主 Goroutine 提前退出的问题,我们需要一种机制来等待其他 Goroutine 执行完毕。`sync.WaitGroup` 是最常用的工具。
- `wg.Add(n)`:增加计数器,表示需要等待 n 个 Goroutine。
- `wg.Done()`:当一个 Goroutine 完成任务时调用,使计数器减一。通常使用 `defer wg.Done()` 来确保执行。
- `wg.Wait()`:阻塞主 Goroutine,直到计数器归零。
### 规范化代码示例:
```go
package main
import (
"fmt"
"sync"
"time"
)
// 定义一个 WaitGroup
var wg sync.WaitGroup
func worker(id int) {
// 在函数退出时,通知 WaitGroup 任务已完成
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// 模拟耗时操作
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
// 启动 3 个 Goroutine
for i := 1; i <= 3; i++ {
// 每启动一个 Goroutine,计数器加 1
wg.Add(1)
go worker(i)
}
fmt.Println("Main: Waiting for workers to finish...")
// 等待所有 Goroutine 完成(即等待计数器归零)
wg.Wait()
fmt.Println("Main: All workers have finished. Exiting.")
}
```
---
# 三、并发中的资源竞争与同步
当多个 Goroutine 同时访问和修改同一个共享资源(如一个变量)时,就会产生**资源竞争 (Race Condition)**,这会导致程序结果不可预测,是并发编程中的常见陷阱。
## 1. 资源竞争问题演示(售票)
下面的代码模拟了多个售票窗口同时售票的场景,由于没有同步机制,会导致超卖或数据不一致。
### 规范化代码示例:
```go
package main
import (
"fmt"
"sync"
"time"
)
var ticket = 10 // 共享资源:总票数
func saleTickets(name string, wg *sync.WaitGroup) {
defer wg.Done()
for {
// 在检查和修改票数之间,可能会发生上下文切换,导致竞争
if ticket > 0 {
// 模拟处理耗时,增加竞争发生概率
time.Sleep(10 * time.Millisecond)
fmt.Printf("%s 售出 1 张票, 剩余 %d 张\n", name, ticket-1)
ticket--
} else {
fmt.Printf("%s 发现票已售罄\n", name)
break
}
}
}
func main() {
var wg sync.WaitGroup
// 启动 4 个售票窗口
wg.Add(4)
go saleTickets("窗口1", &wg)
go saleTickets("窗口2", &wg)
go saleTickets("窗口3", &wg)
go saleTickets("窗口4", &wg)
wg.Wait()
fmt.Println("所有售票窗口关闭。")
}
```
运行上述代码,你会看到类似 `剩余 -1 张` 的错误结果,这就是资源竞争的后果。
## 2. 解决方案一:`sync.Mutex` 互斥锁
互斥锁(Mutex)是一种保护**临界区(Critical Section)**的常用工具,它确保同一时间只有一个 Goroutine 可以访问被保护的共享资源。
- `mutex.Lock()`: 获取锁。如果锁已被其他 Goroutine 持有,则当前 Goroutine 会阻塞,直到可以获取锁。
- `mutex.Unlock()`: 释放锁,允许其他等待的 Goroutine 获取。
### 规范化代码示例(修复后的售票问题):
```go
package main
import (
"fmt"
"sync"
"time"
)
var ticket = 10 // 共享资源
var mutex sync.Mutex // 互斥锁
var wg sync.WaitGroup
func saleTicketsWithLock(name string) {
defer wg.Done()
for {
// 在访问共享资源前加锁
mutex.Lock()
if ticket > 0 {
time.Sleep(10 * time.Millisecond)
fmt.Printf("%s 售出 1 张票, 剩余 %d 张\n", name, ticket-1)
ticket--
// 操作完成后解锁
mutex.Unlock()
} else {
// 如果票已售完,也要解锁,否则会造成死锁,其他Goroutine永远无法进入
mutex.Unlock()
fmt.Printf("%s 发现票已售罄\n", name)
break
}
}
}
func main() {
// 启动 4 个售票窗口
wg.Add(4)
go saleTicketsWithLock("窗口1")
go saleTicketsWithLock("窗口2")
go saleTicketsWithLock("窗口3")
go saleTicketsWithLock("窗口4")
wg.Wait()
fmt.Println("所有售票窗口关闭。")
}
```
## 3. Go 的哲学:通过通信共享内存
您笔记中提到了一句非常重要的话:**"不要以共享内存的方式去通信,而要以通信的方式去共享内存。"** 这是 Go 并发设计的核心哲学。
- **共享内存通信**: 使用锁 (`sync.Mutex`) 保护共享变量,是传统的多线程编程模型。
- **通信共享内存**: 使用 **Channel** 在 Goroutine 之间传递数据(或数据的所有权)。数据在同一时间只被一个 Goroutine 持有,从而天然地避免了资源竞争。这是 Go 更推崇的方式。
### 示例:使用 Channel 控制并发任务
虽然 Channel 也能解决售票问题(实现起来更复杂),但它更常用于生产者-消费者模型。下面是一个简单的例子,展示其基本用法:
```go
package main
import (
"fmt"
"time"
)
// jobs channel 用于发送工作,results channel 用于接收结果
func worker(id int, jobs <-chan int, results chan<- int) {
// 从 jobs channel 接收任务
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second) // 模拟耗时
fmt.Printf("Worker %d finished job %d\n", id, j)
// 将结果发送到 results channel
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// 启动 3 个 worker Goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送 5 个任务到 jobs channel
for j := 1; j <= numJobs; j++ {
jobs <- j
}
// 关闭 jobs channel,表示没有更多任务了
// worker 的 for-range 循环会因此而结束
close(jobs)
// 从 results channel 收集所有结果
for a := 1; a <= numJobs; a++ {
<-results
}
fmt.Println("All jobs are done.")
}
```
---
# 四、`runtime` 包:与 Go 调度器交互
`runtime` 包提供了与 Go 运行时系统交互的能力,例如控制调度器或获取系统信息。
- `runtime.NumCPU()`: 返回当前系统的 CPU 核心数。
- `runtime.GOMAXPROCS(n)`: **[补充知识点]** 设置可同时执行 Go 代码的 CPU 最大数量。自 Go 1.5 起,默认值就是 `runtime.NumCPU()`,通常无需手动设置。
- `runtime.Gosched()`: 让出当前 Goroutine 的执行权限,让调度器去执行其他 Goroutine。当前 Goroutine 会被放回队列,未来会恢复执行。
- `runtime.Goexit()`: 立即终止当前的 Goroutine,但不会影响其他 Goroutine。`defer` 语句在该 Goroutine 中仍会被执行。
### 规范化代码示例:
```go
package main
import (
"fmt"
"runtime"
"sync"
)
func exitDemo() {
defer fmt.Println("Goroutine A: defer is executed")
fmt.Println("Goroutine A: about to exit.")
runtime.Goexit() // 终止当前 Goroutine
fmt.Println("Goroutine A: this line will never be reached.")
}
func main() {
var wg sync.WaitGroup
// --- NumCPU ---
fmt.Printf("System CPU count: %d\n", runtime.NumCPU())
// --- Goexit demo ---
wg.Add(1)
go func() {
defer wg.Done()
exitDemo()
}()
// --- Gosched demo ---
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 2; i++ {
fmt.Println("Goroutine B:", i)
// 让出 CPU,让 main Goroutine 有机会执行
runtime.Gosched()
}
}()
// Main Goroutine 的工作
for i := 0; i < 2; i++ {
fmt.Println("Main Goroutine:", i)
runtime.Gosched()
}
wg.Wait()
fmt.Println("Main Goroutine finished.")
}
```