06:并发模型详解——goroutine、竞争检测与通道通信
约 6176 字大约 21 分钟
2026-04-01
现代计算机早已进入多核时代,但传统编程语言在并发编程上依然困难重重——线程管理复杂、锁机制易错、共享数据难以同步。Go语言从设计之初就将并发作为核心特性,通过轻量级的goroutine和通道(channel)提供了一种全新的并发编程模型。在本篇中,我们将深入Go的并发世界,学习如何创建和管理goroutine,理解调度器的运行机制,掌握同步共享数据的正确方式,并熟练运用通道在goroutine之间安全传递数据。学完本篇,你将能够编写高效、安全且易于维护的并发程序。
本篇核心收获
- 理解并发与并行的区别,掌握Go调度器的工作原理
- 学会使用
go关键字创建goroutine,并利用sync.WaitGroup同步等待 - 掌握竞争状态的检测与修复,使用原子函数和互斥锁保护共享资源
- 深入理解无缓冲通道与有缓冲通道的行为差异
- 通过网球比赛和接力赛等案例,掌握通道在goroutine间的同步与通信
- 学会使用有缓冲通道实现工作池模式
1. 并发与并行
在深入了解Go的并发机制前,我们需要先理清并发(concurrency)和并行(parallelism)这两个核心概念。
1.1 操作系统线程与进程
当运行一个应用程序时,操作系统会为其创建一个进程。进程是一个容器,包含了程序运行所需的各种资源(内存地址空间、文件句柄、线程等)。每个进程至少有一个主线程,线程是操作系统调度执行的最小单元。当主线程终止时,整个应用程序也会终止。

图1:一个运行的应用程序的进程和线程的简要描绘
1.2 Go调度器与逻辑处理器
Go的运行时调度器在操作系统线程之上构建了 逻辑处理器(Logical Processor) 的概念。每个逻辑处理器绑定到一个操作系统线程,而goroutine则在逻辑处理器上运行。Go 1.5版本之后,默认会为每个可用的物理处理器(CPU核心)分配一个逻辑处理器。

图2:Go调度器如何管理goroutine
当创建一个goroutine时,它会被放入调度器的全局运行队列。随后调度器会将goroutine分配给某个逻辑处理器的本地运行队列,等待该逻辑处理器执行。当某个goroutine执行阻塞的系统调用(如文件读取)时,线程和goroutine会从逻辑处理器上分离,该线程继续阻塞,而调度器会创建新线程绑定到该逻辑处理器,继续执行其他goroutine。网络I/O调用则通过集成网络轮询器实现非阻塞等待。
1.3 并发 vs 并行
并发(Concurrency):同时管理很多事情,但可能只在一个物理处理器上交替执行。
并行(Parallelism):同时做很多事情,需要多个物理处理器同时执行。
Go语言通过调度器实现了高效的并发,但要让goroutine真正并行运行,必须使用多个逻辑处理器(通常设为CPU核心数)。但盲目增加逻辑处理器并不一定提升性能,需要根据实际情况测试。
模块小结:Go的并发模型构建在操作系统线程之上,通过逻辑处理器和调度器将goroutine高效地映射到有限的线程上。理解并发与并行的区别有助于设计更合理的程序结构。
2. goroutine——轻量级线程
goroutine是Go并发的基本单位,使用go关键字即可启动一个新的goroutine。下面通过几个示例来观察goroutine的调度行为。
2.1 单逻辑处理器上的并发
代码清单1:单逻辑处理器上并发运行两个goroutine
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
// 分配一个逻辑处理器
runtime.GOMAXPROCS(1)
var wg sync.WaitGroup
wg.Add(2)
fmt.Println("Start Goroutines")
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'a'; char < 'a'+26; char++ {
fmt.Printf("%c", char)
}
}
}()
go func() {
defer wg.Done()
for count := 0; count < 3; count++ {
for char := 'A'; char < 'A'+26; char++ {
fmt.Printf("%c", char)
}
}
}()
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("\nTerminating Program")
}输出示例:
Start Goroutines
Waiting To Finish
ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzTerminating Program可以看到,由于只有一个逻辑处理器,两个goroutine交替运行,但由于第一个goroutine完成得很快,几乎全部大写字母输出后才开始输出小写字母。调度器会在合适的时机切换goroutine。
2.2 调度器的时间片切换
为了让调度器更明显地展示切换,我们可以让goroutine执行耗时操作,例如计算素数。
代码清单2:计算素数展示调度器切换
package main
import (
"fmt"
"runtime"
"sync"
)
var wg sync.WaitGroup
func main() {
runtime.GOMAXPROCS(1)
wg.Add(2)
fmt.Println("Create Goroutines")
go printPrime("A")
go printPrime("B")
fmt.Println("Waiting To Finish")
wg.Wait()
fmt.Println("Terminating Program")
}
func printPrime(prefix string) {
defer wg.Done()
next:
for outer := 2; outer < 5000; outer++ {
for inner := 2; inner < outer; inner++ {
if outer%inner == 0 {
continue next
}
}
fmt.Printf("%s:%d\n", prefix, outer)
}
fmt.Println("Completed", prefix)
}输出片段(每次运行可能不同):
B:2
B:3
...
B:4591
A:3 // 切换goroutine
A:5
...
A:4567
B:4603 // 切换回来
...
图3:goroutine在逻辑处理器的线程上进行交换
2.3 多逻辑处理器并行
通过设置runtime.GOMAXPROCS(runtime.NumCPU()),可以让每个物理处理器上运行一个逻辑处理器,实现真正的并行。
runtime.GOMAXPROCS(runtime.NumCPU())在并行模式下,两个goroutine可能同时运行,输出中大小写字母会混合出现。
模块小结:goroutine的创建和调度非常轻量,通过
runtime.GOMAXPROCS可以控制并发级别。理解调度器的切换行为有助于编写可预测的并发程序。
3. 竞争状态
当多个goroutine同时访问同一个共享资源,且至少有一个进行写操作时,就会产生竞争状态。竞争状态会导致数据不一致或程序崩溃。
3.1 竞争状态示例
代码清单3:存在竞争状态的程序
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
value := counter // 读取
runtime.Gosched() // 让出时间片
value++
counter = value // 写入
}
}输出:
Final Counter: 2期望结果是4,实际却是2。原因在于两个goroutine交替执行时,读取的值可能已被另一个goroutine修改,导致覆盖。

图4:竞争状态下程序行为的图像表达
3.2 检测竞争状态
Go提供了内置的竞争检测器,在编译时加上-race标志即可(注意:windows下不支持):
go build -race
./program输出会明确指出发生数据竞争的代码行和涉及的goroutine。
模块小结:竞争状态是并发程序中最常见的错误之一。通过
go build -race可以轻松检测,但根本上需要在设计时避免共享数据的无序访问。
4. 锁住共享资源
Go提供了两种主要方式来同步对共享资源的访问:原子操作和互斥锁。
4.1 原子函数
sync/atomic包提供了一系列原子操作函数,用于安全地操作整型变量和指针。
代码清单4:使用原子函数修正竞争状态
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Println("Final Counter:", counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
atomic.AddInt64(&counter, 1)
runtime.Gosched()
}
}输出:
Final Counter: 4atomic.AddInt64保证了对counter的加法操作是原子的。类似的还有LoadInt64和StoreInt64,可用于实现安全标志位。
代码清单5:使用原子标志关闭goroutine
var shutdown int64
// 在goroutine中检查
if atomic.LoadInt64(&shutdown) == 1 {
break
}
// 在主goroutine中设置
atomic.StoreInt64(&shutdown, 1)4.2 互斥锁
互斥锁(sync.Mutex)可以在代码中定义一个临界区,保证同一时间只有一个goroutine进入。
代码清单6:使用互斥锁修正竞争状态
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int
wg sync.WaitGroup
mutex sync.Mutex
)
func main() {
wg.Add(2)
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Printf("Final Counter: %d\n", counter)
}
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
mutex.Lock()
{
value := counter
runtime.Gosched()
value++
counter = value
}
mutex.Unlock()
}
}输出:
Final Counter: 4互斥锁将读、修改、写这三步操作包装成一个临界区,确保其他goroutine无法在此期间访问counter。
模块小结:原子函数适用于简单的整型操作,互斥锁则能保护任意代码段。两者都能消除竞争状态,但通道往往能提供更简洁的解决方案。
5. 通道——goroutine间的通信
通道(channel)是Go语言中实现CSP(通信顺序进程, Communication Sequential Process)模型的核心。它允许一个goroutine向另一个goroutine发送数据,并自动处理同步。
5.1 创建与使用通道
// 无缓冲通道
unbuffered := make(chan int)
// 有缓冲通道,缓冲区大小10
buffered := make(chan string, 10)
// 发送数据
buffered <- "Gopher"
// 接收数据
value := <-buffered5.2 无缓冲通道
无缓冲通道在接收前无法保存任何值,要求发送和接收必须同时准备好,否则会阻塞。这种特性天然实现了同步。
网球比赛模拟:使用无缓冲通道在两位"选手"之间传递球。
代码清单7:网球比赛模拟
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
court := make(chan int)
wg.Add(2)
go player("Nadal", court)
go player("Djokovic", court)
court <- 1 // 发球
wg.Wait()
}
func player(name string, court chan int) {
defer wg.Done()
for {
ball, ok := <-court
if !ok {
fmt.Printf("Player %s Won\n", name)
return
}
n := rand.Intn(100)
if n%13 == 0 {
fmt.Printf("Player %s Missed\n", name)
close(court)
return
}
fmt.Printf("Player %s Hit %d\n", name, ball)
ball++
court <- ball
}
}接力比赛模拟:使用无缓冲通道传递接力棒。
代码清单8:接力比赛模拟
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
baton := make(chan int)
wg.Add(1)
go Runner(baton)
baton <- 1
wg.Wait()
}
func Runner(baton chan int) {
var newRunner int
runner := <-baton
fmt.Printf("Runner %d Running With Baton\n", runner)
if runner != 4 {
newRunner = runner + 1
fmt.Printf("Runner %d To The Line\n", newRunner)
go Runner(baton)
}
time.Sleep(100 * time.Millisecond)
if runner == 4 {
fmt.Printf("Runner %d Finished, Race Over\n", runner)
wg.Done()
return
}
fmt.Printf("Runner %d Exchange With Runner %d\n", runner, newRunner)
baton <- newRunner
}输出示例:
Runner 1 Running With Baton
Runner 1 To The Line
Runner 1 Exchange With Runner 2
Runner 2 Running With Baton
Runner 2 To The Line
Runner 2 Exchange With Runner 3
Runner 3 Running With Baton
Runner 3 To The Line
Runner 3 Exchange With Runner 4
Runner 4 Running With Baton
Runner 4 Finished, Race Over
图5:使用无缓冲的通道在goroutine之间同步
5.3 有缓冲通道
有缓冲通道可以存储一个或多个值,只有当缓冲区满时发送才会阻塞,缓冲区空时接收才会阻塞。
工作池模式:固定数量的goroutine从有缓冲通道中领取任务。
代码清单9:工作池模拟
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
const (
numberGoroutines = 4
taskLoad = 10
)
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().Unix())
}
func main() {
tasks := make(chan string, taskLoad)
wg.Add(numberGoroutines)
for gr := 1; gr <= numberGoroutines; gr++ {
go worker(tasks, gr)
}
for post := 1; post <= taskLoad; post++ {
tasks <- fmt.Sprintf("Task: %d", post)
}
close(tasks)
wg.Wait()
}
func worker(tasks chan string, worker int) {
defer wg.Done()
for {
task, ok := <-tasks
if !ok {
fmt.Printf("Worker: %d : Shutting Down\n", worker)
return
}
fmt.Printf("Worker: %d : Started %s\n", worker, task)
sleep := rand.Int63n(100)
time.Sleep(time.Duration(sleep) * time.Millisecond)
fmt.Printf("Worker: %d : Completed %s\n", worker, task)
}
}输出片段:
Worker: 1 : Started Task: 1
Worker: 2 : Started Task: 2
Worker: 3 : Started Task: 3
Worker: 4 : Started Task: 4
Worker: 1 : Completed Task: 1
Worker: 1 : Started Task: 5
...
Worker: 4 : Shutting Down
图6:使用有缓冲的通道在goroutine之间同步数据
模块小结:无缓冲通道强制同步,有缓冲通道提供异步缓冲。选择哪种取决于业务是否需要即时同步。通道的使用让代码更加清晰,减少了锁的复杂性。
6. 本篇核心知识点速记
- goroutine:轻量级线程,使用
go关键字启动。调度器将goroutine映射到逻辑处理器上执行。 - 并发 vs 并行:并发是同时管理多个任务,并行是同时执行多个任务。Go默认使用并发,可通过
runtime.GOMAXPROCS设置并行级别。 - 竞争状态:多个goroutine同时读写共享资源导致数据不一致。使用
go build -race检测。 - 原子操作:
sync/atomic包提供AddInt64、LoadInt64、StoreInt64等,用于安全操作整型变量。 - 互斥锁:
sync.Mutex的Lock/Unlock定义临界区,保证同一时间只有一个goroutine进入。 - 通道:
make(chan T)创建无缓冲通道,make(chan T, capacity)创建有缓冲通道。使用<-发送/接收数据。 - 无缓冲通道:同步交换,发送和接收必须同时就绪。
- 有缓冲通道:异步通信,缓冲区满时发送阻塞,缓冲区空时接收阻塞。
- WaitGroup:
Add设置计数,Done递减,Wait阻塞直到计数归零,用于等待一组goroutine完成。
文末小结
本篇我们系统学习了Go语言的并发模型。从goroutine的创建与调度,到竞争状态的产生与检测,再到原子函数、互斥锁以及通道的使用,我们逐步构建了编写并发程序所需的知识体系。Go通过轻量级的goroutine和优雅的通道机制,让并发编程变得简单、安全且高效。
在实际开发中,应优先考虑使用通道进行通信,而不是通过共享内存来同步。通道鼓励"通过通信来共享内存",而不是"通过共享内存来通信"。这种设计使得并发代码更易于理解与维护。
配套检测题
一、选择题
1. 关于并发与并行的区别,以下说法正确的是:
A. 并发就是并行,两者没有区别
B. 并发是在一个处理器上交替执行多个任务,并行是在多个处理器上同时执行多个任务
C. Go语言只能实现并发,不能实现并行
D. 并行比并发更常用
2. goroutine相比线程的特点,错误的是:
A. 初始栈仅2KB,可动态增长
B. 由Go运行时调度,而非操作系统
C. 可以创建数十万个而不影响性能
D. goroutine是操作系统线程的直接替代品
3. 竞争状态产生的前提条件是:
A. 多个goroutine同时运行
B. 多个goroutine同时访问共享资源,且至少有一个进行写操作
C. 使用了共享变量
D. 程序运行在多逻辑处理器上
4. go build -race的作用是:
A. 加速程序编译
B. 检测竞争状态
C. 运行测试
D. 清理未使用的包
5. 无缓冲通道和有缓冲通道的主要区别是:
A. 无缓冲通道不能存储任何值,有缓冲通道可以存储多个值
B. 无缓冲通道只能发送整型,有缓冲通道可以发送任意类型
C. 无缓冲通道是同步的,有缓冲通道是异步的
D. 有缓冲通道性能更好,应优先使用
二、填空题
6. Go 1.5之后,默认会为每个________分配一个逻辑处理器。
7. 互斥锁使用sync.Mutex的________方法进入临界区,________方法离开。
8. sync/atomic包提供AddInt64用于对整型变量进行________操作。
9. WaitGroup的Add方法用于________计数,Done方法用于________计数。
10. 有缓冲通道在缓冲区________时发送阻塞,在缓冲区________时接收阻塞。
三、问答题
11. 解释Go调度器如何管理goroutine,包括全局运行队列、本地运行队列和逻辑处理器的概念。
12. 说明竞争状态是如何产生的,以及为什么go build -race能够检测到竞争状态。
13. 比较原子函数和互斥锁的适用场景,说明各自优缺点。
14. 解释无缓冲通道如何实现同步,以及这种特性在网球比赛模拟中的应用。
四、编程题
15. 编写并发程序演示竞争状态和修复:
// 1. 定义incCounter函数,增加全局计数器1000次
// 2. 启动10个goroutine并发调用incCounter
// 3. 使用WaitGroup等待所有goroutine完成
// 4. 打印最终计数器值(应该是10000)
// 5. 使用互斥锁修复竞争状态16. 编写程序演示有缓冲通道的工作池模式:
// 1. 创建有缓冲通道,容量为5
// 2. 启动3个worker goroutine
// 3. 向通道发送8个任务
// 4. worker处理任务(模拟耗时),打印任务信息
// 5. 关闭通道,等待所有worker完成17. 编写程序模拟生产者-消费者模式:
// 1. 创建两个通道:一个用于任务,一个用于结果
// 2. 启动2个生产者goroutine,生成数字1-5
// 3. 启动3个消费者goroutine,处理数字(计算平方)
// 4. 主goroutine收集并打印所有结果
// 5. 使用WaitGroup同步五、综合应用题
18. 实现一个并发任务调度系统:
// 1. 定义Task结构体(ID, Description)
// 2. 定义Worker函数,接收任务通道,执行任务
// 3. 创建4个worker组成工作池
// 4. 向工作池发送10个任务
// 5. 使用两个通道:一个用于分发任务,一个用于收集结果
// 6. 实现优雅关闭:发送完任务后关闭任务通道
// 7. 等待所有结果后退出19. 分析以下代码的执行流程、输出结果并说明问题:
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func main() {
ch := make(chan int)
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(50 * time.Millisecond)
}
close(ch)
}()
for v := range ch {
fmt.Printf("Received: %d\n", v)
}
wg.Wait()
fmt.Println("Main done")
}20. 考虑以下场景:需要并发下载100个文件,单个文件下载是独立的任务。请设计程序:
// 1. 说明应该使用有缓冲通道还是无缓冲通道
// 2. 说明worker数量如何确定
// 3. 实现下载函数(模拟)
// 4. 编写完整程序
// 5. 说明这种设计相比串行下载的优势答案
一、选择题答案
1. B 并发是同时管理多个任务(可能交替执行),并行是同时执行多个任务(需要多个处理器)。Go通过runtime.GOMAXPROCS控制。
2. D goroutine不是操作系统线程的直接替代品,它运行在逻辑处理器上,而逻辑处理器绑定到操作系统线程。
3. B 竞争状态需要多个goroutine同时访问共享资源,且至少有一个进行写操作。单纯多goroutine运行或使用共享变量不一定产生竞争状态。
4. B go build -race是竞争检测器,在程序运行时检测数据竞争。Windows下可能不支持。
5. C 无缓冲通道是同步的(发送和接收必须同时就绪),有缓冲通道是异步的(缓冲区满/空时才阻塞)。两者适用场景不同,不能简单说哪个更好。
二、填空题答案
6. CPU核心(物理处理器)
7. Lock;Unlock
8. 原子
9. 增加;减少
10. 满;空
三、问答题答案
11. Go调度器在操作系统线程之上构建了逻辑处理器的抽象。当创建goroutine时,它被放入全局运行队列。调度器将goroutine分配给某个逻辑处理器的本地运行队列。每个逻辑处理器绑定到一个操作系统线程。当goroutine执行阻塞系统调用时,线程和goroutine分离,调度器创建新线程继续执行其他goroutine。网络I/O通过集成网络轮询器实现非阻塞。
12. 竞争状态产生:当多个goroutine同时读取、修改同一个共享变量时,由于操作不是原子的,后写入的操作可能覆盖先写入的操作,导致数据丢失。
go build -race通过在程序运行期间插桩(instrumentation)检测对共享变量的并发访问。它会在每次读写共享变量时记录访问的goroutine ID和时间戳,如果检测到多个goroutine对同一变量的读写存在竞争(一个写,另一个同时读写),则报告。
13. 原子函数适用于:简单的整型变量操作(计数器、标志位等),性能高,开销小。但只能保护单个变量的操作。
互斥锁适用于:保护复杂的代码段(包含多个变量的操作,或复杂的逻辑),灵活性高。但需要小心锁的粒度,避免死锁。
选择原则:如果能使用原子操作就不要用锁,原子操作更简单且性能更好。
14. 无缓冲通道在接收前无法保存任何值,发送和接收必须同时准备好,否则阻塞。这种特性天然实现了同步:发送方必须等待接收方准备好,接收方必须等待发送方发送数据。
在网球比赛模拟中,通道代表球场。选手A发球(发送)后必须等待选手B准备好接球(接收),选手B接球后必须将球打回(发送),同时选手A准备接球(接收)。这种同步机制保证了两人交替击球,不会出现一人连续击球的情况。
四、编程题答案
15.
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex
wg sync.WaitGroup
)
func incCounter() {
defer wg.Done()
for i := 0; i < 1000; i++ {
mutex.Lock()
{
counter++
}
mutex.Unlock()
}
}
func main() {
wg.Add(10)
for i := 0; i < 10; i++ {
go incCounter()
}
wg.Wait()
fmt.Printf("Final Counter: %d\n", counter)
}16.
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(tasks chan string, id int) {
defer wg.Done()
for {
task, ok := <-tasks
if !ok {
fmt.Printf("Worker %d: Shutting Down\n", id)
return
}
fmt.Printf("Worker %d: Started %s\n", id, task)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Worker %d: Completed %s\n", id, task)
}
}
func main() {
tasks := make(chan string, 5)
wg.Add(3)
for i := 1; i <= 3; i++ {
go worker(tasks, i)
}
for i := 1; i <= 8; i++ {
tasks <- fmt.Sprintf("Task: %d", i)
}
close(tasks)
wg.Wait()
fmt.Println("All workers done")
}17.
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func producer(tasks chan<- int, start, end int) {
defer wg.Done()
for i := start; i <= end; i++ {
tasks <- i
}
}
func consumer(tasks <-chan int, results chan<- int) {
defer wg.Done()
for {
task, ok := <-tasks
if !ok {
return
}
results <- task * task
}
}
func main() {
tasks := make(chan int, 10)
results := make(chan int, 10)
wg.Add(5)
go producer(tasks, 1, 5)
go producer(tasks, 6, 10)
go consumer(tasks, results)
go consumer(tasks, results)
go consumer(tasks, results)
go func() {
wg.Wait()
close(tasks)
close(results)
}()
for r := range results {
fmt.Println(r)
}
}五、综合应用题答案
18.
package main
import (
"fmt"
"sync"
)
type Task struct {
ID int
Description string
}
var wg sync.WaitGroup
func worker(taskCh <-chan Task, resultCh chan<- string, id int) {
defer wg.Done()
for {
task, ok := <-taskCh
if !ok {
fmt.Printf("Worker %d: Stopped\n", id)
return
}
fmt.Printf("Worker %d: Processing %s\n", id, task.Description)
resultCh <- fmt.Sprintf("Worker %d completed: %s", id, task.Description)
}
}
func main() {
taskCh := make(chan Task, 10)
resultCh := make(chan string, 10)
wg.Add(4)
for i := 1; i <= 4; i++ {
go worker(taskCh, resultCh, i)
}
for i := 1; i <= 10; i++ {
taskCh <- Task{ID: i, Description: fmt.Sprintf("Task-%d", i)}
}
close(taskCh)
done := make(chan struct{})
go func() {
wg.Wait()
close(resultCh)
close(done)
}()
for result := range resultCh {
fmt.Println(result)
}
<-done
fmt.Println("All tasks completed")
}19. 输出结果:
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Main done执行流程:
- 主goroutine启动匿名goroutine,wg.Add(1)
- 主goroutine进入for range循环等待通道数据
- 匿名goroutine循环发送0-4,每次sleep 50ms
- 主goroutine接收并打印每个数字
- 发送完5个数字后,匿名goroutine关闭通道
- for range检测到通道关闭,自动退出
- wg.Wait()返回,主goroutine打印"Main done"
问题:无。程序正确使用了WaitGroup和通道的close机制。
20.
package main
import (
"fmt"
"sync"
"time"
)
type File struct {
ID int
Name string
}
var wg sync.WaitGroup
func download(file File) {
fmt.Printf("Downloading %s\n", file.Name)
time.Sleep(100 * time.Millisecond)
fmt.Printf("Completed %s\n", file.Name)
}
func worker(files <-chan File) {
defer wg.Done()
for {
file, ok := <-files
if !ok {
return
}
download(file)
}
}
func main() {
files := make(chan File, 10)
workerCount := 5
wg.Add(workerCount)
for i := 0; i < workerCount; i++ {
go worker(files)
}
for i := 1; i <= 100; i++ {
files <- File{ID: i, Name: fmt.Sprintf("file-%d.txt", i)}
}
close(files)
wg.Wait()
fmt.Println("All downloads completed")
}设计说明:
- 选择有缓冲通道:任务数量多(100个),工作池需要预存任务,有缓冲通道可以缓冲任务,让生产者不必等待消费者立即处理。
- worker数量:通常设置为CPU核心数或略高(本例设为5)。
- 优势相比串行:
- 并发下载,充分利用网络带宽
- 工作池模式,控制并发数,避免资源耗尽
- 代码结构清晰,易于维护和扩展
