【问题标题】:Different results for N>1 goroutines (on N>1 Cpu:s). Why?N>1 个 goroutine 的不同结果(在 N>1 Cpu:s 上)。为什么?
【发布时间】:2013-06-10 12:38:10
【问题描述】:

我有一个测试程序,当在多个 Cpu (Goroutines = Cpus) 上执行多个 goroutine 时会给出不同的结果。 “测试”是关于使用通道同步 goroutines,程序本身计算字符串中字符的出现次数。它在一个 Cpu / 一个 goroutine 上产生一致的结果。

查看 Playground 上的代码示例(注意:在本地机器上运行以在多核上执行,并观察结果数字的变化):http://play.golang.org/p/PT5jeCKgBv

代码摘要:该程序计算 (DNA) 字符串中 4 个不同字符(A、T、G、C)的出现次数。

问题:在多个 Cpu(goroutines)上执行时,结果(出现 n 个字符)会有所不同。为什么?

说明

  1. goroutine 将工作 (SpawnWork) 作为字符串生成到 Workers。设置 人工字符串输入数据(硬编码字符串被复制 n 次)。
  2. Goroutine Workers (Worker) 的创建等于 Cpu 的数量。
  3. Workers 检查字符串中的每个字符并计算 A、T 并发送 总和到一个通道,G,C 计数到另一个通道。
  4. SpawnWork 关闭工作字符串通道以控制 Workers(使用范围消耗字符串,当输入通道被 SpawnWork 关闭时退出)。
  5. Workers 消耗了它的范围(字符)时,它会在退出通道上发送一个退出信号(退出
  6. Main (select) 循环将在收到 Cpu-count 退出次数后退出 信号。
  7. Main func 打印字符出现的摘要(A、T、G、C)。

简化代码

1.“工人”(goroutines)按行计数字符:

func Worker(inCh chan *[]byte, resA chan<- *int, resB chan<- *int, quit chan bool) {
    //for p_ch := range inCh {
    for {
        p_ch, ok := <-inCh // similar to range
        if ok {
            ch := *p_ch
            for i := 0; i < len(ch); i++ {
                if ch[i] == 'A' || ch[i] == 'T' {        // Count A:s and T:s
                    at++
                } else if ch[i] == 'G' || ch[i] == 'C' { // Count G:s and C:s
                    gc++
                }
            }
            resA <- &at  // Send line results on separate channels
            resB <- &gc  // Send line results on separate channels
        } else {
            quit <- true // Indicate that we're all done
            break
        }
    }
}

2. 将工作(字符串)发送给工作人员:

func SpawnWork(inStr chan<- *[]byte, quit chan bool) {
    // Artificial input data
    StringData :=
        "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN\n" +
        "NTGAGAAATATGCTTTCTACTTTTTTGTTTAATTTGAACTTGAAAACAAAACACACACAA\n" +
        "... etc\n" +
    // ...
    for scanner.Scan() {
        s := scanner.Bytes()
        if len(s) == 0 || s[0] == '>' {
            continue
        } else {
            i++
            inStr <- &s
        }
    }
    close(inStr) // Indicate (to Workers) that there's no more strings coming.
}

3. 主程序:

func main() {
    // Count Cpus, and count down in final select clause
    CpuCnt := runtime.NumCPU() 
    runtime.GOMAXPROCS(CpuCnt)
    // Make channels
    resChA := make(chan *int)
    resChB := make(chan *int)
    quit := make(chan bool)
    inStr := make(chan *[]byte)

    // Set up Workers ( n = Cpu )
    for i := 0; i < CpuCnt; i++ {
        go Worker(inStr, resChA, resChB, quit)
    }
    // Send lines to Workers
    go SpawnWork(inStr, quit)

    // Count the number of "A","T" & "G","C" per line 
    // (comes in here as ints per row, on separate channels (at and gt))
    for {
        select {
        case tmp_at := <-resChA:
            tmp_gc := <-resChB // Ch A and B go in pairs anyway
            A += *tmp_at       // sum of A's and T's
            B += *tmp_gc       // sum of G's and C's
        case <-quit:
            // Each goroutine sends "quit" signals when it's done. Since 
            // the number of goroutines equals the Cpu counter, we count 
            // down each time a goroutine tells us it's done (quit at 0):
            CpuCnt--
            if CpuCnt == 0 { // When all goroutines are done then we're done.
                goto out     
            }
        }
    }
out:
    // Print report to screen
}

为什么这段代码只有在单个 cpu/goroutine 上执行时才会一致?也就是说,通道似乎没有同步,或者主循环在所有 goroutine 完成之前强制退出?挠头。

(再次:在操场上查看/运行完整代码:http://play.golang.org/p/PT5jeCKgBv

// 罗尔夫·兰帕

【问题讨论】:

  • 你试过用go run -race运行它吗? golang.org/doc/articles/race_detector.html
  • 我查看了代码并没有发现任何问题,所以我自己尝试了-race。它发现了一些问题,但其中之一是Worker 开头的at 变量暴露给main,因为您执行tmp_at := &lt;-resChA。也许如果你通过通道传递数字本身(而不是指向数字的指针)那么它就不会很活泼?
  • @MatrixFog:我实际上已经忘记了 -race 选项。嗬!但是,删除指针并不能消除比赛,这真的让我很困惑。我会继续缩小范围。
  • 请参阅下面与 Nick Craig-Wood 讨论的所有问题,无论是明显的还是(可能)隐藏的问题。一个人吸取了很多教训……好吧,去吧。 :)
  • 将 goroutine 的数量与 CPU 的数量捆绑在一起是一种冒险的策略——它有时可能会导致 CPU 的利用率不足,尤其是在涉及 I/O 的情况下。在你的情况下,你可能很幸运。另一种策略是使用“过度并行”——​​即 goroutine 比 CPU 多。即使其中一些被阻塞(例如在通道上),CPU 内核也可以继续执行其他一些。

标签: concurrency go channel


【解决方案1】:

这是一个工作版本,无论使用多少 CPU,都能始终如一地产生相同的结果。

这就是我所做的

  • 删除*int 的传递 - 在频道中传递非常活泼!
  • 删除 *[]byte 的传递 - 毫无意义,因为切片无论如何都是引用类型
  • 在将切片放入通道之前复制切片 - 切片指向相同的内存,从而导致争用
  • 修复 Workeratgc 的初始化 - 它们位于错误的位置 - 这是导致结果不同的主要原因
  • 使用sync.WaitGroup 进行同步和通道关闭()

我使用-race parameter of go build 来查找和修复数据争用。

package main

import (
    "bufio"
    "fmt"
    "runtime"
    "strings"
    "sync"
)

func Worker(inCh chan []byte, resA chan<- int, resB chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Worker started...")
    for ch := range inCh {
        at := 0
        gc := 0
        for i := 0; i < len(ch); i++ {
            if ch[i] == 'A' || ch[i] == 'T' {
                at++
            } else if ch[i] == 'G' || ch[i] == 'C' {
                gc++
            }
        }
        resA <- at
        resB <- gc
    }

}

func SpawnWork(inStr chan<- []byte) {
    fmt.Println("Spawning work:")
    // An artificial input source.
    StringData :=
        "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN\n" +
            "NTGAGAAATATGCTTTCTACTTTTTTGTTTAATTTGAACTTGAAAACAAAACACACACAA\n" +
            "CTTCCCAATTGGATTAGACTATTAACATTTCAGAAAGGATGTAAGAAAGGACTAGAGAGA\n" +
            "TATACTTAATGTTTTTAGTTTTTTAAACTTTACAAACTTAATACTGTCATTCTGTTGTTC\n" +
            "AGTTAACATCCCTGAATCCTAAATTTCTTCAGATTCTAAAACAAAAAGTTCCAGATGATT\n" +
            "TTATATTACACTATTTACTTAATGGTACTTAAATCCTCATTNNNNNNNNCAGTACGGTTG\n" +
            "TTAAATANNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN\n" +
            "NNNNNNNCTTCAGAAATAAGTATACTGCAATCTGATTCCGGGAAATATTTAGGTTCATAA\n"
    // Expand data n times
    tmp := StringData
    for n := 0; n < 1000; n++ {
        StringData = StringData + tmp
    }
    scanner := bufio.NewScanner(strings.NewReader(StringData))
    scanner.Split(bufio.ScanLines)

    var i int
    for scanner.Scan() {
        s := scanner.Bytes()
        if len(s) == 0 || s[0] == '>' {
            continue
        } else {
            i++
            s_copy := append([]byte(nil), s...)
            inStr <- s_copy
        }
    }
    close(inStr)
}

func main() {
    CpuCnt := runtime.NumCPU() // Count down in select clause
    CpuOut := CpuCnt           // Save for print report
    runtime.GOMAXPROCS(CpuCnt)
    fmt.Printf("Processors: %d\n", CpuCnt)

    resChA := make(chan int)
    resChB := make(chan int)
    inStr := make(chan []byte)

    fmt.Println("Spawning workers:")
    var wg sync.WaitGroup
    for i := 0; i < CpuCnt; i++ {
        wg.Add(1)
        go Worker(inStr, resChA, resChB, &wg)
    }
    fmt.Println("Spawning work:")
    go func() {
        SpawnWork(inStr)
        wg.Wait()
        close(resChA)
        close(resChB)
    }()

    A := 0
    B := 0
    LineCnt := 0
    for tmp_at := range resChA {
        tmp_gc := <-resChB // Theese go together anyway
        A += tmp_at
        B += tmp_gc
        LineCnt++
    }

    if !(A+B > 0) {
        fmt.Println("No A/B was found!")
    } else {
        ABFraction := float32(B) / float32(A+B)
        fmt.Println("\n----------------------------")
        fmt.Printf("Cpu's  : %d\n", CpuOut)
        fmt.Printf("Lines  : %d\n", LineCnt)
        fmt.Printf("A+B    : %d\n", A+B)
        fmt.Printf("A      : %d\n", A)
        fmt.Printf("B      : %d\n", A)
        fmt.Printf("AB frac: %v\n", ABFraction*100)
        fmt.Println("----------------------------")
    }
}

【讨论】:

  • 非常感谢您的详尽回答!不幸的是,指针本身似乎不是问题(删除所有 ptrs 并不能消除比赛)。种族问题的确切(也是唯一)原因是与 s (字符串)的冲突,这是由于扫描仪在读取工作人员时替换它而引起的。您还使用 s 的副本在代码中解决了这个问题( "s_copy := append([]byte(nil), s...)" )。非常感谢您指出这一点!还要感谢您与等待组的替代策略。金子! :)
  • 除了发现上面的竞争问题之外,我想指出为什么我的示例中的所有指针(它们实际上是有道理的)。我使用(语言)性能测试中的这个示例算法来读取 DNA 序列。 Ptrs 是产生显着差异的优化。看看 Go-lang 与 C、C++、D、Pascal、Phyton 等相比的表现可能会很有趣(超级优化(go)速度:gist.github.com/samuell/5591369)。比较:saml.rilspace.org/…(Go 做得很好!):)
  • 提示:如果您指出比赛问题是扫描字符串(您复制的),那么我可以将您的答案认可为“答案”。是的,当试图替换“for ... range”循环(它最初在循环内)时,worker 中变量的初始化被意外移动了。因此,提出这两个修复可以解决整个问题,即使将指针和所有其余部分保持在原始代码中也是如此。
  • *[]byte*int 指针都可能导致竞争(并且竞争检测器同意),但由于您的频道没有缓冲,所以这次没有!尝试使用缓冲通道的示例(给make(chan XXX, 1000) 提供第二个参数),您会看到我怀疑的比赛开始发挥作用。您还会看到性能提升。
  • 很高兴为您服务!有趣的是,种族检测器并没有检测到所有种族——一个很好的实验。退出的渠道策略很好 - 我认为 WaitGroup 更整洁 - 也许我不应该改变它,因为它没有直接说明修复,但我无法抗拒尝试让代码更漂亮!
猜你喜欢
  • 2022-09-26
  • 1970-01-01
  • 1970-01-01
  • 2011-01-29
  • 2020-10-19
  • 2011-02-16
  • 1970-01-01
  • 1970-01-01
  • 2021-10-01
相关资源
最近更新 更多