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

FunTester · March 26, 2025 · 1385 hits

并发编程是 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: 0
FunTester 并行 goroutine: 0
FunTester 并发 goroutine: 1
FunTester 并行 goroutine: 1
FunTester 并发 goroutine: 2
FunTester 并行 goroutine: 2
FunTester 并发 goroutine: 3
FunTester 并行 goroutine: 3
FunTester 并发 goroutine: 4
FunTester 并行 goroutine: 4
FunTester: 所有 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: 计算任务 0
FunTester: 计算任务 1
FunTester: 计算任务 2
FunTester: 计算任务 3
FunTester: 计算任务 4
FunTester: 并发执行耗时 103.456ms
FunTester: 计算任务 0
FunTester: 计算任务 1
FunTester: 计算任务 2
FunTester: 计算任务 3
FunTester: 计算任务 4
FunTester: 串行执行耗时 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=49999995000000
FunTester: CPU 密集型任务 1 完成,sum=49999995000000
FunTester: 所有 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=49999995000000
FunTester: CPU 密集型任务 1 完成,sum=49999995000000
FunTester: CPU 密集型任务 2 完成,sum=49999995000000
FunTester: 所有 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=49999995000000
FunTester: CPU 密集型任务 1 完成,sum=49999995000000
FunTester: CPU 密集型任务 2 完成,sum=49999995000000
FunTester: 所有 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 的产生。

FunTester 原创精华
【免费合集】从 Java 开始性能测试
故障测试与 Web 前端
服务端功能测试
性能测试专题
Java、Groovy、Go
白盒、工具、爬虫、UI 自动化
理论、感悟、视频
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
No Reply at the moment.
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up