写点什么

Go 语言常见错误——并发编程

作者:FunTester
  • 2025-03-26
    河北
  • 本文字数:13438 字

    阅读完需:约 44 分钟

并发编程是 Go 语言的一大亮点,得益于 goroutine 和 channel 等特性,Go 在并发处理上提供了简洁而强大的工具。然而,尽管 Go 的并发模型易于使用,但开发者在实际编程中常常会遇到一些常见错误,如 goroutine 的泄露、竞争条件的产生、channel 使用不当等问题,这些错误往往会导致程序的逻辑错误或性能瓶颈。


本模块将深入分析 Go 语言并发编程中的常见错误,帮助开发者更好地理解 goroutine 和 channel 的工作原理,以及如何避免并发编程中的陷阱。通过对实际错误的剖析,读者将能掌握如何编写更加稳定和高效的并发代码,提升程序的性能和可维护性。

错误五十五:混淆并发和并行 (#55)

示例代码:


package main
import ( "fmt" "runtime" "time")
func main() { runtime.GOMAXPROCS(1) // 限制为单核执行
// 并发示例 go func() { for i := 0; i < 5; i++ { fmt.Println("FunTester 并发 goroutine:", i) time.Sleep(100 * time.Millisecond) } }()
// 并行示例 for i := 0; i < 5; i++ { fmt.Println("FunTester 主 goroutine:", i) time.Sleep(100 * time.Millisecond) }
time.Sleep(1 * time.Second)}
复制代码


错误说明:许多开发者在 Go 中混淆了并发(Concurrency)和并行(Parallelism)的概念。并发是指能够同时处理多个任务,但不一定同时执行;并行则是指在多核处理器上同时执行多个任务。理解二者的本质区别有助于更有效地设计和优化程序。


可能的影响:混淆并发和并行可能导致程序性能不佳或资源浪费。例如,误以为并发总是并行,可能在单核环境下设计了不必要的 goroutine,增加了上下文切换的开销,反而降低了程序的执行效率。


最佳实践:


  • 明确概念:并发是关于结构设计,允许程序处理多个任务;并行是关于执行,利用多核同时运作多个任务。

  • 性能优化:根据实际需求和环境,合理选择并发或并行。例如,对于 IO 密集型任务,使用大量 goroutine 能有效提高效率;对于 CPU 密集型任务,限制 goroutine 数量以匹配 CPU 核心数,避免过度切换。

  • 测试与基准:通过基准测试(Benchmarking)验证并发与并行方案的实际效果,确保选择最适合的模型。


改进后的代码:


理解并正确区分并发与并行:


package main
import ( "fmt" "runtime" "sync" "time")
func main() { // 设置 GOMAXPROCS 为机器的核心数,实现真正的并行 runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup wg.Add(2)
// 并发任务 go func() { defer wg.Done() for i := 0; i < 5; i++ { fmt.Println("FunTester 并发 goroutine:", i) time.Sleep(100 * time.Millisecond) } }()
// 并行任务 go func() { defer wg.Done() for i := 0; i < 5; i++ { fmt.Println("FunTester 并行 goroutine:", i) time.Sleep(100 * time.Millisecond) } }()
wg.Wait() fmt.Println("FunTester: 所有 goroutine 完成")}
复制代码


输出结果:


FunTester 并发 goroutine: 0FunTester 并行 goroutine: 0FunTester 并发 goroutine: 1FunTester 并行 goroutine: 1FunTester 并发 goroutine: 2FunTester 并行 goroutine: 2FunTester 并发 goroutine: 3FunTester 并行 goroutine: 3FunTester 并发 goroutine: 4FunTester 并行 goroutine: 4FunTester: 所有 goroutine 完成
复制代码

错误五十六:认为并发总是更快 (#56)

示例代码:


package main
import ( "fmt" "runtime" "sync" "time")
func compute(i int) { time.Sleep(100 * time.Millisecond) fmt.Println("FunTester: 计算任务", i)}
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() compute(i) }(i) }
wg.Wait() elapsedConcurrent := time.Since(start) fmt.Printf("FunTester: 并发执行耗时 %v\n", elapsedConcurrent)
// 串行执行 start = time.Now() for i := 0; i < 5; i++ { compute(i) } elapsedSequential := time.Since(start) fmt.Printf("FunTester: 串行执行耗时 %v\n", elapsedSequential)}
复制代码


错误说明:许多开发者错误地认为并发总是比串行更快。实际上,并发适用于多个任务可以重叠执行的场景,但并不保证一定提高性能,尤其是在任务较轻或系统资源有限的情况下。


可能的影响:在不适合并发的场景下使用并发,可能导致程序性能下降。比如,在任务量较小或系统资源紧张时,创建过多的 goroutine 反而增加了调度和上下文切换的开销,影响整体执行效率。


最佳实践:


  • 性能测试:在决定并发与否前,通过基准测试(Benchmarking)评估不同方案的性能,避免盲目采用并发导致性能下降。

  • 适度并发:根据任务特性和系统资源,合理控制 goroutine 的数量。对于 CPU 密集型任务,goroutine 数量应接近 CPU 核心数;对于 IO 密集型任务,可以适当增加 goroutine 数量。

  • 资源评估:评估并发方案对内存和其他资源的影响,确保不会因资源耗尽导致程序崩溃或性能急剧下降。


改进后的代码:


通过基准测试验证并发是否提高性能:


package main
import ( "fmt" "runtime" "sync" "time")
func compute(i int) { time.Sleep(100 * time.Millisecond) fmt.Println("FunTester: 计算任务", i)}
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) var wg sync.WaitGroup
// 并发执行 start := time.Now()
for i := 0; i < 5; i++ { wg.Add(1) go func(i int) { defer wg.Done() compute(i) }(i) }
wg.Wait() elapsedConcurrent := time.Since(start) fmt.Printf("FunTester: 并发执行耗时 %v\n", elapsedConcurrent)
// 串行执行 start = time.Now() for i := 0; i < 5; i++ { compute(i) } elapsedSequential := time.Since(start) fmt.Printf("FunTester: 串行执行耗时 %v\n", elapsedSequential)}
复制代码


输出结果:


FunTester: 计算任务 0FunTester: 计算任务 1FunTester: 计算任务 2FunTester: 计算任务 3FunTester: 计算任务 4FunTester: 并发执行耗时 103.456msFunTester: 计算任务 0FunTester: 计算任务 1FunTester: 计算任务 2FunTester: 计算任务 3FunTester: 计算任务 4FunTester: 串行执行耗时 502.789ms
复制代码


分析:在本示例中,5 个并发任务的总耗时约 103ms,而串行执行耗时约 503ms。由于每个 compute 函数执行时间固定且能够并行执行,因此并发显著提高了性能。然而,这并不意味着并发总是更快,具体效果还需根据实际场景评估。

错误五十七:不清楚何时使用 channels 或 mutexes (#57)

示例代码:


package main
import ( "fmt" "sync")
type FunTester struct { Name string Age int}
func main() { testers := []FunTester{ {Name: "FunTester1", Age: 25}, {Name: "FunTester2", Age: 30}, }
// 使用 channels 进行同步(不正确的场景) ch := make(chan bool, len(testers)) for i := 0; i < len(testers); i++ { go func(t *FunTester) { t.Age += 1 ch <- true }(&testers[i]) }
for i := 0; i < len(testers); i++ { <-ch }
fmt.Println("FunTester: 修改后的 testers =", testers)
// 使用 mutexes 进行并发安全修改 var mu sync.Mutex testersMutex := []FunTester{ {Name: "FunTester1", Age: 25}, {Name: "FunTester2", Age: 30}, }
var wg sync.WaitGroup wg.Add(len(testersMutex)) for i := 0; i < len(testersMutex); i++ { go func(i int) { defer wg.Done() mu.Lock() testersMutex[i].Age += 1 mu.Unlock() }(i) }
wg.Wait() fmt.Println("FunTester: Mutex 修改后的 testers =", testersMutex)}
复制代码


错误说明:在 Go 中,选择使用 channels 还是 mutexes 取决于具体的并发需求。一般来说,mutexes 适用于共享资源的同步访问,而 channels 更适合 goroutines 之间的通信和协调。混淆二者的用途可能导致代码复杂化或性能问题。


可能的影响:


  • 使用 channels 进行同步访问:虽然可行,但会导致不必要的复杂性和性能开销。channels 更适合传递数据,而非简单的同步控制。

  • 使用 mutexes 进行通信:mutexes 仅用于同步访问,无法实现 goroutines 之间的数据传递。


最佳实践:


  • 使用 mutexes

  • 当需要保护共享变量的访问时,使用 sync.Mutexsync.RWMutex

  • 适用于需要多个 goroutine 并发读取或写入同一资源的场景。

  • 使用 channels

  • 当需要在 goroutines 之间传递数据或信号时,使用 channels。

  • 适用于需要协调多个 goroutine 的执行顺序或传递控制信号的场景。

  • 遵循通讯顺序优于共享内存的原则,尽量通过 channels 进行数据交换,减少使用共享变量。


改进后的代码:


清晰地使用 channels 和 mutexes 分别用于其适合的场景:


package main
import ( "fmt" "sync")
type FunTester struct { Name string Age int}
func main() { fmt.Println("=== 使用 Channels 进行 goroutine 间的同步 ===") testers := []FunTester{ {Name: "FunTester1", Age: 25}, {Name: "FunTester2", Age: 30}, }
// 使用 channels 进行同步 done := make(chan bool) for i := 0; i < len(testers); i++ { go func(t *FunTester) { t.Age += 1 done <- true }(&testers[i]) }
// 等待所有 goroutine 完成 for i := 0; i < len(testers); i++ { <-done }
fmt.Println("FunTester: 修改后的 testers =", testers)
fmt.Println("\n=== 使用 Mutexes 进行共享资源的同步访问 ===") testersMutex := []FunTester{ {Name: "FunTester1", Age: 25}, {Name: "FunTester2", Age: 30}, }
var mu sync.Mutex var wg sync.WaitGroup wg.Add(len(testersMutex)) for i := 0; i < len(testersMutex); i++ { go func(i int) { defer wg.Done() mu.Lock() testersMutex[i].Age += 1 mu.Unlock() }(i) }
wg.Wait() fmt.Println("FunTester: Mutex 修改后的 testers =", testersMutex)}
复制代码


输出结果:


=== 使用 Channels 进行 goroutine 间的同步 ===FunTester: 修改后的 testers = [{FunTester1 26} {FunTester2 31}]
=== 使用 Mutexes 进行共享资源的同步访问 ===FunTester: Mutex 修改后的 testers = [{FunTester1 26} {FunTester2 31}]
复制代码

错误五十八:不明白竞态问题 (数据竞态 vs. 竞态条件和 Go 内存模型) (#58)

示例代码:


package main
import ( "fmt" "sync")
type FunTester struct { Name string Age int}
func main() { var wg sync.WaitGroup wg.Add(2)
var ft FunTester
go func() { defer wg.Done() ft.Name = "FunTesterA" }()
go func() { defer wg.Done() ft.Age = 30 }()
wg.Wait() fmt.Printf("FunTester: %+v\n", ft)}
复制代码


错误说明:数据竞态(Data Race)和竞态条件(Race Condition)是并发编程中常见的问题。数据竞态指的是多个 goroutine 同时访问同一内存区域,且至少有一个写操作,而没有合适的同步机制;而竞态条件则是程序的行为依赖于 goroutine 的执行顺序,导致不可预测的结果。理解二者的区别有助于正确处理并发问题。


可能的影响:


  • 数据竞态:会导致内存数据的不一致,程序行为不可预测,甚至程序崩溃。go run -race 可以检测到数据竞态。

  • 竞态条件:可能导致逻辑错误,程序无法按照预期运行。虽然不一定是数据竞态,但会使得程序结果不稳定。


最佳实践:


  • 避免数据竞态

  • 使用同步原语(如 sync.Mutexsync.RWMutex)保护共享数据。

  • 使用 channels 进行 goroutine 之间的通信,避免直接共享内存。

  • 使用 go run -race 工具检测和修复数据竞态。

  • 避免竞态条件

  • 确保关键操作的执行顺序,通过同步机制(如 wait groups、信号量)控制 goroutine 的执行。

  • 设计无状态或不可变的数据结构,减少依赖执行顺序。

  • 彻底理解 Go 的内存模型,确保在多 goroutine 环境下的操作安全。


改进后的代码:


使用 sync.Mutex 避免数据竞态,并明确竞态条件的处理:


package main
import ( "fmt" "sync")
type FunTester struct { Name string Age int}
func main() { var wg sync.WaitGroup wg.Add(2)
var ft FunTester var mu sync.Mutex
go func() { defer wg.Done() mu.Lock() ft.Name = "FunTesterA" mu.Unlock() }()
go func() { defer wg.Done() mu.Lock() ft.Age = 30 mu.Unlock() }()
wg.Wait() fmt.Printf("FunTester: %+v\n", ft)}
复制代码


输出结果:


FunTester: {Name:FunTesterA Age:30}
复制代码


使用 go run -race 检测数据竞态:在修复前,运行以下命令:


go run -race main.go
复制代码


如果存在数据竞态,会输出类似:


WARNING: DATA RACE
复制代码


修复后,确认没有竞态相关警告。

错误五十九:不理解不同工作负载类型对并发的影响 (#59)

示例代码:


package main
import ( "fmt" "runtime" "sync" "time")
func cpuIntensiveTask(id int) { sum := 0 for i := 0; i < 1e7; i++ { sum += i } fmt.Printf("FunTester: CPU 密集型任务 %d 完成,sum=%d\n", id, sum)}
func ioIntensiveTask(id int) { fmt.Printf("FunTester: IO 密集型任务 %d 开始\n", id) time.Sleep(500 * time.Millisecond) fmt.Printf("FunTester: IO 密集型任务 %d 完成\n", id)}
func main() { runtime.GOMAXPROCS(runtime.NumCPU()) var wg sync.WaitGroup
fmt.Println("FunTester: 开始 CPU 密集型任务") for i := 0; i < runtime.NumCPU(); i++ { wg.Add(1) go func(id int) { defer wg.Done() cpuIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 CPU 密集型任务完成")
fmt.Println("\nFunTester: 开始 IO 密集型任务") for i := 0; i < 5; i++ { // 增加 goroutine 数量 wg.Add(1) go func(id int) { defer wg.Done() ioIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 IO 密集型任务完成")}
复制代码


错误说明:不同类型的工作负载对并发的适合度不同。CPU 密集型任务和 IO 密集型任务在并发设计上有不同的考量。误解或忽视这些差异,可能导致资源利用不当,影响程序性能。


可能的影响:


  • CPU 密集型任务:如果 goroutine 数量远超 CPU 核心数,会导致过多的上下文切换,反而降低性能。

  • IO 密集型任务:因为 IO 操作通常会阻塞,可以适当增加 goroutine 数量,以充分利用 CPU 资源,提高效率。


最佳实践:


  • CPU 密集型任务

  • 根据 CPU 核心数合理设置 goroutine 数量,通常与 GOMAXPROCS 相近。

  • 避免过度创建 goroutine,减少上下文切换开销。

  • IO 密集型任务

  • 可以创建大量 goroutine,因为大部分时间会被阻塞在 IO 操作上。

  • 利用 goroutine 的轻量特性,提高程序的并发能力和资源利用率。

  • 基准测试

  • 通过基准测试(Benchmark)评估不同工作负载下的并发方案,找到最优配置。


改进后的代码:


根据不同工作负载类型调整 goroutine 数量:


package main
import ( "fmt" "runtime" "sync" "time")
func cpuIntensiveTask(id int) { sum := 0 for i := 0; i < 1e7; i++ { sum += i } fmt.Printf("FunTester: CPU 密集型任务 %d 完成,sum=%d\n", id, sum)}
func ioIntensiveTask(id int) { fmt.Printf("FunTester: IO 密集型任务 %d 开始\n", id) time.Sleep(500 * time.Millisecond) fmt.Printf("FunTester: IO 密集型任务 %d 完成\n", id)}
func main() { cpuCores := runtime.NumCPU() runtime.GOMAXPROCS(cpuCores) var wg sync.WaitGroup
fmt.Println("FunTester: 开始 CPU 密集型任务") for i := 0; i < cpuCores; i++ { wg.Add(1) go func(id int) { defer wg.Done() cpuIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 CPU 密集型任务完成")
fmt.Println("\nFunTester: 开始 IO 密集型任务") for i := 0; i < 100; i++ { // 增加 goroutine 数量,根据需要调整 wg.Add(1) go func(id int) { defer wg.Done() ioIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 IO 密集型任务完成")}
复制代码


输出结果:


FunTester: 开始 CPU 密集型任务FunTester: CPU 密集型任务 0 完成,sum=49999995000000FunTester: CPU 密集型任务 1 完成,sum=49999995000000FunTester: 所有 CPU 密集型任务完成
FunTester: 开始 IO 密集型任务FunTester: IO 密集型任务 0 开始FunTester: IO 密集型任务 1 开始...FunTester: IO 密集型任务 99 开始FunTester: IO 密集型任务 0 完成FunTester: IO 密集型任务 1 完成...FunTester: IO 密集型任务 99 完成FunTester: 所有 IO 密集型任务完成
复制代码

错误五十九:不理解不同工作负载类型对并发的影响 (#59)

示例代码:


package main
import ( "fmt" "runtime" "sync" "time")
func cpuIntensiveTask(id int) { sum := 0 for i := 0; i < 1e7; i++ { sum += i } fmt.Printf("FunTester: CPU 密集型任务 %d 完成,sum=%d\n", id, sum)}
func ioIntensiveTask(id int) { fmt.Printf("FunTester: IO 密集型任务 %d 开始\n", id) time.Sleep(500 * time.Millisecond) fmt.Printf("FunTester: IO 密集型任务 %d 完成\n", id)}
func main() { cpuCores := runtime.NumCPU() runtime.GOMAXPROCS(cpuCores) var wg sync.WaitGroup
fmt.Println("FunTester: 开始 CPU 密集型任务") for i := 0; i < cpuCores; i++ { wg.Add(1) go func(id int) { defer wg.Done() cpuIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 CPU 密集型任务完成")
fmt.Println("\nFunTester: 开始 IO 密集型任务") for i := 0; i < 100; i++ { // 增加 goroutine 数量,根据需要调整 wg.Add(1) go func(id int) { defer wg.Done() ioIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 IO 密集型任务完成")}
复制代码


错误说明:不同类型的工作负载对并发模型的适用性有不同的影响。理解工作负载的性质(CPU 密集型还是 IO 密集型)有助于合理配置 goroutine 数量和使用合适的同步机制,提升程序的整体性能和资源利用率。


可能的影响:


  • CPU 密集型任务:如果 goroutine 数量过多,会导致频繁的上下文切换,增加 CPU 负载,降低程序性能。

  • IO 密集型任务:可以通过增加 goroutine 数量,充分利用等待 IO 的时间,提高程序的吞吐量和响应能力。


最佳实践:


  • 评估任务类型:在设计并发模型前,评估任务是 CPU 密集型还是 IO 密集型。

  • 调整 goroutine 数量

  • CPU 密集型:goroutine 数量应与 CPU 核心数相近,避免过多导致上下文切换开销。

  • IO 密集型:goroutine 数量可以适当增加,以充分利用 IO 等待时间,提升并发能力。

  • 资源管理:监控资源使用情况,调整并发配置以达到最佳性能。


改进后的代码:


根据工作负载类型合理调整 goroutine 数量:


package main
import ( "fmt" "runtime" "sync" "time")
func cpuIntensiveTask(id int) { sum := 0 for i := 0; i < 1e7; i++ { sum += i } fmt.Printf("FunTester: CPU 密集型任务 %d 完成,sum=%d\n", id, sum)}
func ioIntensiveTask(id int) { fmt.Printf("FunTester: IO 密集型任务 %d 开始\n", id) time.Sleep(500 * time.Millisecond) fmt.Printf("FunTester: IO 密集型任务 %d 完成\n", id)}
func main() { cpuCores := runtime.NumCPU() runtime.GOMAXPROCS(cpuCores) var wg sync.WaitGroup
fmt.Println("FunTester: 开始 CPU 密集型任务") for i := 0; i < cpuCores; i++ { wg.Add(1) go func(id int) { defer wg.Done() cpuIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 CPU 密集型任务完成")
fmt.Println("\nFunTester: 开始 IO 密集型任务") // 根据 IO 密集型任务的特性,增加 goroutine 数量 ioGoroutines := 100 for i := 0; i < ioGoroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() ioIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 IO 密集型任务完成")}
复制代码


输出结果:


FunTester: 开始 CPU 密集型任务FunTester: CPU 密集型任务 0 完成,sum=49999995000000FunTester: CPU 密集型任务 1 完成,sum=49999995000000FunTester: CPU 密集型任务 2 完成,sum=49999995000000FunTester: 所有 CPU 密集型任务完成
FunTester: 开始 IO 密集型任务FunTester: IO 密集型任务 0 开始FunTester: IO 密集型任务 1 开始...FunTester: IO 密集型任务 99 完成FunTester: 所有 IO 密集型任务完成
复制代码

错误五十八:不明白竞态问题 (数据竞态 vs. 竞态条件和 Go 内存模型) (#58)

示例代码:


package main
import ( "fmt" "sync")
type FunTester struct { Name string Age int}
func main() { var wg sync.WaitGroup wg.Add(2)
var ft FunTester
go func() { defer wg.Done() ft.Name = "FunTesterA" }()
go func() { defer wg.Done() ft.Age = 30 }()
wg.Wait() fmt.Printf("FunTester: %+v\n", ft)}
复制代码


错误说明:在并发编程中,数据竞态和竞态条件是两个不同的概念。数据竞态(Data Race)指的是多个 goroutine 同时访问同一内存区域,且至少有一个写操作,而没有适当的同步;竞态条件指的是程序的行为依赖于 goroutine 的执行顺序,可能导致不可预测的结果。理解二者的区别对于正确设计并发程序至关重要。


可能的影响:


  • 数据竞态:导致数据不一致、程序崩溃,甚至引发安全漏洞。使用 go run -race 可以检测到数据竞态,但需要通过同步机制解决。

  • 竞态条件:导致程序逻辑错误,结果不稳定。可能不是数据竞态,但仍需通过合理的同步和设计避免。


最佳实践:


  • 避免数据竞态

  • 使用 sync.Mutexsync.RWMutex 保护共享变量。

  • 使用 channels 进行 goroutine 间的通信,避免直接共享内存。

  • 使用原子操作(sync/atomic)处理简单的同步场景。

  • 使用 go run -race 工具检测数据竞态并修复。

  • 避免竞态条件

  • 确保关键操作的执行顺序,通过同步机制控制 goroutine 的执行。

  • 设计无状态或不可变的数据结构,减少对执行顺序的依赖。

  • 熟悉 Go 的内存模型,理解顺序和同步的底层保证。


改进后的代码:


使用 sync.Mutex 保护共享变量,避免数据竞态:


package main
import ( "fmt" "sync")
type FunTester struct { Name string Age int}
func main() { var wg sync.WaitGroup wg.Add(2)
var ft FunTester var mu sync.Mutex
go func() { defer wg.Done() mu.Lock() ft.Name = "FunTesterA" mu.Unlock() }()
go func() { defer wg.Done() mu.Lock() ft.Age = 30 mu.Unlock() }()
wg.Wait() fmt.Printf("FunTester: %+v\n", ft)}
复制代码


输出结果:


FunTester: {Name:FunTesterA Age:30}
复制代码


说明:通过使用 sync.Mutex,确保在任一时刻只有一个 goroutine 能够修改 ft 对象,从而避免数据竞态问题。同时,程序行为变得确定,不依赖于 goroutine 的执行顺序,避免了竞态条件。

错误五十九:不理解不同工作负载类型对并发的影响 (#59)

示例代码:


package main
import ( "fmt" "runtime" "sync" "time")
func cpuIntensiveTask(id int) { sum := 0 for i := 0; i < 1e7; i++ { sum += i } fmt.Printf("FunTester: CPU 密集型任务 %d 完成,sum=%d\n", id, sum)}
func ioIntensiveTask(id int) { fmt.Printf("FunTester: IO 密集型任务 %d 开始\n", id) time.Sleep(500 * time.Millisecond) fmt.Printf("FunTester: IO 密集型任务 %d 完成\n", id)}
func main() { cpuCores := runtime.NumCPU() runtime.GOMAXPROCS(cpuCores) var wg sync.WaitGroup
fmt.Println("FunTester: 开始 CPU 密集型任务") for i := 0; i < cpuCores; i++ { wg.Add(1) go func(id int) { defer wg.Done() cpuIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 CPU 密集型任务完成")
fmt.Println("\nFunTester: 开始 IO 密集型任务") for i := 0; i < 100; i++ { // 增加 goroutine 数量 wg.Add(1) go func(id int) { defer wg.Done() ioIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 IO 密集型任务完成")}
复制代码


错误说明:不同类型的工作负载对并发模型的适用性有不同的影响。理解工作负载的性质(CPU 密集型还是 IO 密集型)有助于合理配置 goroutine 数量和使用合适的同步机制,提升程序的整体性能和资源利用率。


可能的影响:


  • CPU 密集型任务:如果 goroutine 数量过多,会导致频繁的上下文切换,增加 CPU 负载,降低程序性能。

  • IO 密集型任务:可以通过增加 goroutine 数量,充分利用等待 IO 的时间,提高程序的吞吐量和响应能力。


最佳实践:


  • 评估任务类型:在设计并发模型前,评估任务是 CPU 密集型还是 IO 密集型。

  • 调整 goroutine 数量

  • CPU 密集型:goroutine 数量应与 CPU 核心数相近,避免过多导致上下文切换开销。

  • IO 密集型:goroutine 数量可以适当增加,以充分利用 IO 等待时间,提升并发能力。

  • 资源管理:监控资源使用情况,调整并发配置以达到最佳性能。


改进后的代码:


根据工作负载类型合理调整 goroutine 数量:


package main
import ( "fmt" "runtime" "sync" "time")
func cpuIntensiveTask(id int) { sum := 0 for i := 0; i < 1e7; i++ { sum += i } fmt.Printf("FunTester: CPU 密集型任务 %d 完成,sum=%d\n", id, sum)}
func ioIntensiveTask(id int) { fmt.Printf("FunTester: IO 密集型任务 %d 开始\n", id) time.Sleep(500 * time.Millisecond) fmt.Printf("FunTester: IO 密集型任务 %d 完成\n", id)}
func main() { cpuCores := runtime.NumCPU() runtime.GOMAXPROCS(cpuCores) var wg sync.WaitGroup
fmt.Println("FunTester: 开始 CPU 密集型任务") for i := 0; i < cpuCores; i++ { wg.Add(1) go func(id int) { defer wg.Done() cpuIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 CPU 密集型任务完成")
fmt.Println("\nFunTester: 开始 IO 密集型任务") // 根据 IO 密集型任务的特性,增加 goroutine 数量 ioGoroutines := 100 for i := 0; i < ioGoroutines; i++ { wg.Add(1) go func(id int) { defer wg.Done() ioIntensiveTask(id) }(i) } wg.Wait() fmt.Println("FunTester: 所有 IO 密集型任务完成")}
复制代码


输出结果:


FunTester: 开始 CPU 密集型任务FunTester: CPU 密集型任务 0 完成,sum=49999995000000FunTester: CPU 密集型任务 1 完成,sum=49999995000000FunTester: CPU 密集型任务 2 完成,sum=49999995000000FunTester: 所有 CPU 密集型任务完成
FunTester: 开始 IO 密集型任务FunTester: IO 密集型任务 0 开始FunTester: IO 密集型任务 1 开始...FunTester: IO 密集型任务 99 完成FunTester: 所有 IO 密集型任务完成
复制代码


说明:通过根据工作负载类型调整 goroutine 数量,确保 CPU 密集型任务不过度分配 goroutine,而 IO 密集型任务能充分利用并发特性,提高程序性能。

错误六十:误解了 Go contexts (#60)

示例代码:


package main
import ( "context" "fmt" "time")
func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel()
go func(ctx context.Context) { select { case <-ctx.Done(): fmt.Println("FunTester: goroutine 接收到取消信号") return } }(ctx)
time.Sleep(1 * time.Second) fmt.Println("FunTester: 取消上下文") cancel()
time.Sleep(500 * time.Millisecond)}
复制代码


错误说明:Go 的 context(上下文)是并发编程中的重要工具,用于携带截止时间、取消信号和键值对。然而,许多开发者对 context 的理解存在误区,比如不当的传递、过早的取消或滥用上下文,导致程序逻辑错误或资源泄漏。就像错用了钥匙,无法正确开启门锁,导致进退两难。


可能的影响:


  • 资源泄漏:未正确取消上下文,可能导致 goroutine 持续运行,消耗系统资源。

  • 逻辑错误:上下文取消的不当时机,可能导致任务提前终止或延迟取消,影响程序逻辑的正确性。

  • 僵尸 goroutine:goroutine 无法响应取消信号,长期占用资源,影响程序的稳定性。


最佳实践:


  • 传递上下文:将 context 作为函数的首个参数传递,并在需要的地方传递下去,遵循上下文的传播规则。

  • 适时取消:在操作完成或遇到错误时,及时调用取消函数,防止资源泄漏。

  • 独立使用:避免在多个不同用途的函数之间共享同一个上下文,保持上下文的独立性和目的性。

  • 遵循设计:不应在库函数中创建新的根上下文,应始终接收并使用传入的上下文。


改进后的代码:


正确传递和使用 context,确保 goroutine 能够正确响应取消信号:


package main
import ( "context" "fmt" "time")
func worker(ctx context.Context, id int) { for { select { case <-ctx.Done(): fmt.Printf("FunTester: goroutine %d 接收到取消信号,退出\n", id) return default: // 模拟工作 fmt.Printf("FunTester: goroutine %d 正在工作\n", id) time.Sleep(200 * time.Millisecond) } }}
func main() { // 创建带有取消功能的上下文 ctx, cancel := context.WithCancel(context.Background())
// 启动多个 goroutine for i := 1; i <= 3; i++ { go worker(ctx, i) }
// 运行 1 秒后取消上下文 time.Sleep(1 * time.Second) fmt.Println("FunTester: 取消上下文") cancel()
// 等待 goroutine 退出 time.Sleep(500 * time.Millisecond) fmt.Println("FunTester: 程序结束")}
复制代码


输出结果:


FunTester: goroutine 1 正在工作FunTester: goroutine 2 正在工作FunTester: goroutine 3 正在工作FunTester: goroutine 1 正在工作FunTester: goroutine 2 正在工作FunTester: goroutine 3 正在工作FunTester: goroutine 1 正在工作FunTester: goroutine 2 正在工作FunTester: goroutine 3 正在工作FunTester: 取消上下文FunTester: goroutine 1 接收到取消信号,退出FunTester: goroutine 2 接收到取消信号,退出FunTester: goroutine 3 接收到取消信号,退出FunTester: 程序结束
复制代码


说明:通过正确地传递和使用 context,确保 goroutine 能够及时响应取消信号,防止资源泄漏和僵尸 goroutine 的产生。

发布于: 2 小时前阅读数: 7
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Go 语言常见错误——并发编程_FunTester_InfoQ写作社区