并发编程是 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,增加了上下文切换的开销,反而降低了程序的执行效率。

最佳实践:

改进后的代码:

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

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 反而增加了调度和上下文切换的开销,影响整体执行效率。

最佳实践:

改进后的代码:

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

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 和 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 的执行顺序,导致不可预测的结果。理解二者的区别有助于正确处理并发问题。

可能的影响:

最佳实践:

改进后的代码:

使用 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 密集型任务在并发设计上有不同的考量。误解或忽视这些差异,可能导致资源利用不当,影响程序性能。

可能的影响:

最佳实践:

改进后的代码:

根据不同工作负载类型调整 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 数量和使用合适的同步机制,提升程序的整体性能和资源利用率。

可能的影响:

最佳实践:

改进后的代码:

根据工作负载类型合理调整 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 的执行顺序,可能导致不可预测的结果。理解二者的区别对于正确设计并发程序至关重要。

可能的影响:

最佳实践:

改进后的代码:

使用 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 数量和使用合适的同步机制,提升程序的整体性能和资源利用率。

可能的影响:

最佳实践:

改进后的代码:

根据工作负载类型合理调整 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 的理解存在误区,比如不当的传递、过早的取消或滥用上下文,导致程序逻辑错误或资源泄漏。就像错用了钥匙,无法正确开启门锁,导致进退两难。

可能的影响:

最佳实践:

改进后的代码:

正确传递和使用 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 自动化
理论、感悟、视频


↙↙↙阅读原文可查看相关链接,并与作者交流