# 一、核心概念:进程、线程与协程 - **进程 (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.") } ```