You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用goroutines与sync.WaitGroup统计质数结果不一致问题求助

Goroutines统计质数结果不一致的问题排查与修复

问题背景

尝试用Go的goroutines统计小于等于整数i的质数数量,比如i=100时正确结果是25。但当i较大(如10000、1000000)时,每次运行结果都不一致且错误,数值波动随i增大而明显。

原实现代码:

package main

import (
    "fmt"
    "math"
    "sync"
    "time"
)

var wg sync.WaitGroup

func isprime(x int) bool {
    if x == 2 {
        return true
    }
    if x == 1 || x%2 == 0 {
        return false
    }
    var xi = float64(x)
    for i := 3; float64(i) < (math.Pow(xi, 0.5) + 1.0); i += 2 {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func main() {
    fmt.Print("Till what number should I count primes? ")
    var i int
    fmt.Scan(&i)

    r := 0
    pr := &r
    fmt.Println("Counting primes till ", i)
    start := time.Now()
    for x := 0; x <= i; x++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            if isprime(n) {
                *pr += 1
            }
        }(x)
    }
    wg.Wait()
    elapsed := time.Since(start).Seconds()
    fmt.Println("Counted", r, "primes")
    fmt.Println("took", elapsed, "seconds")
}

运行异常示例:

❯ ./main
Till what number should I count primes? 10000
Counting primes till  10000
Counted 1228 primes
took 0.006776541 seconds
❯ ./main
Till what number should I count primes? 10000
Counting primes till  10000
Counted 1227 primes
took 0.004183875 seconds
❯ ./main
Till what number should I count primes? 1000000
Counting primes till  1000000
Counted 78254 primes
took 0.441985921 seconds
❯ ./main
Till what number should I count primes? 1000000
Counting primes till  1000000
Counted 78327 primes
took 0.430042047 seconds

问题原因

核心是竞态条件(Race Condition):多个goroutine同时对共享变量r(通过指针pr访问)执行*pr +=1操作,而这个操作不是原子的——它会被Go编译器拆成三个步骤:

  1. 读取*pr的当前值到寄存器
  2. 寄存器中的值加1
  3. 将新值写回*pr

当多个goroutine同时执行这三步时,会出现互相覆盖的情况:比如goroutine A读取了值10,goroutine B也读取了值10,两者都加1变成11,然后都写回,最终*pr只增加了1,而不是2,导致计数偏少或波动。

小数值时偶尔正确是因为goroutine数量少,冲突概率低,但数值越大,goroutine越多,冲突概率越高,结果偏差就越明显。

修复方案

方案1:使用互斥锁(sync.Mutex)

通过互斥锁保证同一时间只有一个goroutine能修改共享变量,避免竞态:

修改后的代码:

package main

import (
    "fmt"
    "math"
    "sync"
    "time"
)

var wg sync.WaitGroup
var mutex sync.Mutex // 新增互斥锁

func isprime(x int) bool {
    if x == 2 {
        return true
    }
    if x == 1 || x%2 == 0 {
        return false
    }
    var xi = float64(x)
    for i := 3; float64(i) < (math.Pow(xi, 0.5) + 1.0); i += 2 {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func main() {
    fmt.Print("Till what number should I count primes? ")
    var i int
    fmt.Scan(&i)

    r := 0
    pr := &r
    fmt.Println("Counting primes till ", i)
    start := time.Now()
    for x := 0; x <= i; x++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            if isprime(n) {
                mutex.Lock() // 修改前加锁
                *pr += 1
                mutex.Unlock() // 修改后解锁
            }
        }(x)
    }
    wg.Wait()
    elapsed := time.Since(start).Seconds()
    fmt.Println("Counted", r, "primes")
    fmt.Println("took", elapsed, "seconds")
}

方案2:使用原子操作(sync/atomic)

原子操作由底层硬件支持,能保证+1操作的原子性,性能比互斥锁更优:

修改后的代码:

package main

import (
    "fmt"
    "math"
    "sync"
    "sync/atomic"
    "time"
)

var wg sync.WaitGroup

func isprime(x int) bool {
    if x == 2 {
        return true
    }
    if x == 1 || x%2 == 0 {
        return false
    }
    var xi = float64(x)
    for i := 3; float64(i) < (math.Pow(xi, 0.5) + 1.0); i += 2 {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func main() {
    fmt.Print("Till what number should I count primes? ")
    var i int
    fmt.Scan(&i)

    var r int32 = 0 // 改用int32类型适配原子操作
    fmt.Println("Counting primes till ", i)
    start := time.Now()
    for x := 0; x <= i; x++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            if isprime(n) {
                atomic.AddInt32(&r, 1) // 原子加1
            }
        }(x)
    }
    wg.Wait()
    elapsed := time.Since(start).Seconds()
    fmt.Println("Counted", r, "primes")
    fmt.Println("took", elapsed, "seconds")
}

方案3:使用通道(Channel)收集结果

每个goroutine将质数判断结果发送到通道,主goroutine统一统计,完全避免共享变量:

修改后的代码:

package main

import (
    "fmt"
    "math"
    "sync"
    "time"
)

var wg sync.WaitGroup

func isprime(x int) bool {
    if x == 2 {
        return true
    }
    if x == 1 || x%2 == 0 {
        return false
    }
    var xi = float64(x)
    for i := 3; float64(i) < (math.Pow(xi, 0.5) + 1.0); i += 2 {
        if x%i == 0 {
            return false
        }
    }
    return true
}

func main() {
    fmt.Print("Till what number should I count primes? ")
    var i int
    fmt.Scan(&i)

    resultChan := make(chan bool, i+1) // 带缓冲的通道避免阻塞
    fmt.Println("Counting primes till ", i)
    start := time.Now()
    for x := 0; x <= i; x++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            resultChan <- isprime(n)
        }(x)
    }
    // 启动goroutine关闭通道,避免主goroutine阻塞
    go func() {
        wg.Wait()
        close(resultChan)
    }()
    // 统计结果
    r := 0
    for res := range resultChan {
        if res {
            r++
        }
    }
    elapsed := time.Since(start).Seconds()
    fmt.Println("Counted", r, "primes")
    fmt.Println("took", elapsed, "seconds")
}

验证

三种方案都能保证结果一致且正确,比如i=10000时正确结果是1229,i=1000000时正确结果是78498,运行后会稳定输出这个数值。

内容的提问来源于stack exchange,提问作者libkush

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 14:35:10