【问题标题】:What is the best way for concurrent data collection in Go, and why? [closed]Go 中并发数据收集的最佳方式是什么,为什么? [关闭]
【发布时间】:2021-03-03 13:15:37
【问题描述】:

我在实践中发现了两种收集数据的方法。我的同事使用不同的方法。但是,github 上的大多数项目(例如 vault)都使用第二种方式。我无法决定哪个更好。你能解释一下为什么一种方法比另一种更好吗?

代码:https://play.golang.org/p/OKV7pAwyGuQ

package main

import (
    "fmt"
    "sync"
)

// complex request
func getData() string {
    return "test"
}

func main() {
    // first way
    var wg1 sync.WaitGroup
    var mu1 sync.Mutex
    res1 := make([]string, 0, 10)
    for i := 0; i < 10; i++ {
        wg1.Add(1)
        go func(wg *sync.WaitGroup, mu *sync.Mutex) {
            defer wg.Done()

            mu.Lock()
            res1 = append(res1, getData())
            mu.Unlock()

        }(&wg1, &mu1)
    }

    wg1.Wait()
    fmt.Println(res1)

    // second way
    var wg2 sync.WaitGroup
    ch := make(chan string, 10)
    for i := 0; i < 10; i++ {
        wg2.Add(1)
        go func(wg *sync.WaitGroup, ch chan string) {
            defer wg.Done()

            ch <- getData()
        }(&wg2, ch)
    }

    wg2.Wait()
    close(ch)

    res2 := make([]string, 0, 10)
    for data := range ch {
        res2 = append(res2, data)
    }

    fmt.Println(res2)
}

【问题讨论】:

    标签: go concurrency goroutine


    【解决方案1】:

    您的问题的问题在于,“更好”一词的使用需要一组用于进行比较的标准。在日常生活中,这些标准通常是隐含的,并由交流的人共享,因此单独使用“更好”一词是可以的。 (但即便如此,当你听到说“这辆车比那辆车好”时——你确定说话的人在汽车上和你分享相同的单词视图吗?或者让我们考虑一下“Python 比 Go 更好”这句话——它意味着响应“更好地完成什么任务?”。) 在工程中,问“什么更好”需要严格定义提问者的标准,而你的问题没有;基本上这就是它被否决的原因。

    如果我们考虑您的两个示例,它们大多是相同的,差异可以总结如下:

    • 第一个更新生成的数据结构,通过显式锁定保护操作。
    • 第二个使用隐式锁将数据缓冲在中间数据结构中,一旦收集到数据,就会将其复制到生成的数据结构中。

    再次,哪个更好?

    如果我们考虑可读性和维护成本,前者的行数更少,但它有一个明确的锁定操作,有时会令人不悦¹。
    后者具有更多的行数,并且据说具有更大的 WTF 因子(因为读者可能会对看似不必要的缓冲的使用感到困惑——见下文)。

    如果我们考虑性能,我推测前一种方法会胜出(以微弱优势)。
    原因是从多个 goroutine 访问通道确实使用了同步——尽管它隐藏在任何运行时代码中,这些代码被编译以代替那些 &lt;--&gt; 操作,而且我不希望通道上的争用花费的 CPU 周期少于对互斥体的争用。
    然后考虑在完成所有收集 goroutine 之后,在第二种情况下,您将数据放置在缓冲通道中,然后您将其铲入生成的切片中 - 这是与通道混合的(逐步)内存副本访问——在无竞争的情况下会很快,但每个通道访问必须确保没有竞争者和“锁定”,然后在读取操作期间“解锁”通道的内部结构,并更新每一步的通道长度。

    再说一遍:如果我们谈论的是 10 条复制成本低的类型的数据,那么性能差异将是微乎其微的,我会选择最好的。
    但如果它真的是一个模型示例,并且在将是 100k 数据单元或 1e6 条数据的生产代码中,情况会有所不同。

    附带说明,请考虑在第一个示例中,在持有锁的同时调用 getData() 可能不会通过代码审查。


    TL;DR
    当性能看起来不是问题时,优先考虑可读性而不是性能。
    否则measure.
    如果更喜欢可读性,still mind performance


    ¹ 那是因为如果在获取锁之后调用了一些代码,出现恐慌,锁可能会被保持;考虑:

    s.mu.Lock()
    res = append(res, fooBar())
    s.mu.Unlock()
    

    这里,如果 fooBar() 发生恐慌,s.mu.Unlock() 将不会被调用。
    显然,这可以通过将整个事情分解成一个辅助函数并在那里推迟互斥锁解锁语句来解决。
    更好的是,在这段代码中,最好将获取值拆分为 appendappending 本身:因为 append 不会崩溃(好吧,除非它无法分配内存,但在 Go 程序中这将是一个崩溃),只需将其包装在这样的锁定+解锁组合中是安全的。
    然而,有些人会认为这是不行的,因为有人可能想在 lock 和 unlock 语句之间添加更多代码,然后我们又回到了原点。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-04-30
      • 1970-01-01
      • 2021-05-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-10-03
      • 1970-01-01
      相关资源
      最近更新 更多