352 lines
10 KiB
Markdown
352 lines
10 KiB
Markdown
# 一、核心概念:进程、线程与协程
|
||
|
||
- **进程 (Process)**
|
||
- **定义**: 程序的一次执行过程,是操作系统进行**资源分配**的基本单位。
|
||
- **特点**: 拥有独立的内存空间,创建和切换开销巨大,资源占用多。
|
||
- **线程 (Thread)**
|
||
- **定义**: 进程内的执行单元,是 **CPU 调度**的基本单位。一个进程至少包含一个主线程。
|
||
- **特点**: 共享进程的内存空间,创建和切换开销相比进程小,但仍有相当成本。多线程共享数据时需要处理同步问题(如锁)。
|
||
- **协程 (Coroutine / Goroutine)**
|
||
- **定义**: Go 语言实现的特有并发体,是一种**用户态的、极其轻量级的线程**。
|
||
- **特点**:
|
||
- **开销极小**: 创建成本远低于线程,内存占用仅为KB级别(初始约2KB),可以轻松创建数十万甚至上百万个。
|
||
- **调度灵活**: 由 Go 语言的运行时(Runtime)进行调度,而非操作系统。调度器能够在 I/O 操作等场景下自动切换,实现高效的并发。
|
||
- **非抢占式**: 协程出让 CPU 控制权通常发生在特定的时机(如Channel操作、系统调用、函数调用等),而不是由操作系统强制中断。
|
||
|
||
## 对比总结
|
||
|
||
| 特性 | 进程 (Process) | 线程 (Thread) | **协程 (Goroutine)** |
|
||
| :------- | :------------- | :----------------- | :------------------------ |
|
||
| **角色** | 资源分配单位 | CPU 调度单位 | **Go语言的并发执行体** |
|
||
| **开销** | 巨大 | 较大 | **极小** |
|
||
| **数量级** | 百级 | 万级 | **百万级** |
|
||
| **调度方** | 操作系统 | 操作系统 | **Go 运行时 (Runtime)** |
|
||
| **内存** | 独立 | 共享进程内存 | **共享进程内存** |
|
||
|
||
---
|
||
|
||
# 二、Goroutine:Go 并发编程的基石
|
||
|
||
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.")
|
||
}
|
||
```
|