为什么顺序循环在Go中比并发方法运行得更快?

时间:2018-06-24 02:50:30

标签: go concurrency

场景: 我想快速阅读大型文本文件(例如,在下面的示例中,lorem.txt的行数为4.5毫米)。我在下面的代码中尝试了三种不同的方式。

  1. 非并发顺序处理
  2. 频道和goroutines
  3. 等待组和goroutines

伪基准: 这是我在下面运行此命令时得到的典型输出。下面的输出是快速而肮脏的时间增量,而不是完整的性能分析/基准测试/测试。

sequential:     43.541828091 secs 4714074 lines
queued channel: 80.986544385 secs 4714074 lines
wait group:     260.200473751 secs 4712266 lines

问题: 为什么顺序循环比下面的其他两种方法快?我想念什么吗?

更新 我在更大的文本文件上运行了示例代码(请参见场景)。我还“重置”了每个示例的记录器,以防万一不这样做,示例函数之间会出现一些复合内存问题。另外,正如其他人指出的那样,我的计算机是双核的,这可能是我的代码中的许多问题之一。感谢您的所有反馈意见/答案。

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "log"
    "os"
    "sync"
    "time"
)

var (
    textFile = "lorem.txt"
    buf      bytes.Buffer
    l        = log.New(&buf, "logger: ", log.Lshortfile)
    wg       sync.WaitGroup
    delta1   float64
    delta2   float64
    delta3   float64
    cnt1     = 0
    cnt2     = 0
    cnt3     = 0
)

func main() {

    // Wait Group Example
    exampleWaitGroup()

    // Queued Channel Example
    exampleQueuedChannel()

    // Sequential Loop Example
    exampleSequentialLoop()

    benchmarks := fmt.Sprintf("sequential:\t%v secs %v lines\nqueued channel:\t%v secs %v lines\nwait group:\t%v secs %v lines\n",
        delta1, cnt1,
        delta2, cnt2,
        delta3, cnt3,
    )

    fmt.Println(benchmarks)

}

func exampleSequentialLoop() {

    buf.Reset()
    l = log.New(&buf, "logger: ", log.Lshortfile)

    start := time.Now()

    file1, err := os.Open(textFile)
    if err != nil {
        log.Fatal(err)
    }

    defer file1.Close()

    scanner := bufio.NewScanner(file1)

    for scanner.Scan() {
        cnt1++
        l.Println(scanner.Text())
    }

    end := time.Now()
    delta1 = end.Sub(start).Seconds()

}

func exampleQueuedChannel() {

    buf.Reset()
    l = log.New(&buf, "logger: ", log.Lshortfile)

    start := time.Now()
    queue := make(chan string)
    done := make(chan bool)

    go processQueue(queue, done)

    file2, err := os.Open(textFile)
    if err != nil {
        log.Fatal(err)
    }

    defer file2.Close()

    scanner := bufio.NewScanner(file2)

    for scanner.Scan() {
        queue <- scanner.Text()
    }

    end := time.Now()
    delta2 = end.Sub(start).Seconds()
}

func exampleWaitGroup() {

    buf.Reset()
    l = log.New(&buf, "logger: ", log.Lshortfile)

    start := time.Now()

    file3, err := os.Open(textFile)
    if err != nil {
        log.Fatal(err)
    }

    defer file3.Close()

    scanner := bufio.NewScanner(file3)

    for scanner.Scan() {
        wg.Add(1)
        go func(line string) {
            defer wg.Done()
            l.Println(line)
            cnt3++
        }(scanner.Text())
    }

    wg.Wait()

    end := time.Now()
    delta3 = end.Sub(start).Seconds()

}

func processQueue(queue chan string, done chan bool) {
    for line := range queue {
        l.Println(line)
        cnt2++
    }
    done <- true
}

3 个答案:

答案 0 :(得分:5)

exampleQueuedChannel中,您没有并行执行任何操作。是的,您已经启动了另一个goroutine,但是没有并行处理。原因是queue是一个无缓冲通道。当您写入无缓冲的chan时,写入器将阻塞,直到有人将其读出为止。因此,基本上,您在编写时会阻塞,然后调度程序必须使goroutine进入睡眠状态,然后唤醒读取goroutine。然后那个人睡着了,作家又醒了。因此,您在两个goroutine之间苦苦挣扎,而调度程序正进行繁重的锻炼。
如果要在此处获得更好的性能,请使用缓冲通道。而且,如果您想获得更高的性能,请在每条chan消息 中添加多个项目(有关通道的性能影响的详细技术说明,请阅读this

exampleWaitGroup中,您将为每行启动一个新的goroutine。虽然启动新的goroutine并不昂贵,但它也不是免费的,并且对于调度程序来说还需要更多工作。 defer is also not free。也是您的记录器uses a mutex,因此,如果您的两个goroutine尝试同时登录,则其中一个将进入睡眠状态,并且还会有更多的调度程序工作。

您可以通过launching your code under a profiler自行调查这些问题,并调查瓶颈所在。

答案 1 :(得分:2)

在回答问题之前,我想指出您的基准测试方法有问题。我不会说200行的文件太大或足够用于基准测试。 Go中存在基准测试的“官方”方式,您可以在testing的文档中阅读。

有一个Go成语,可能是最著名的说:并发不是并行性。人们最期望使程序运行更快的是并行,而不是并发。实际上,在不可能实现并行的单核CPU上,并发性通常会使事情变慢,因为在goroutine(线程,协程等)之间进行切换会产生成本

在您的代码中,这很像单核的情况。并行不多,但在goroutine之间却有很多切换。此外,fmt.Println包含IO操作,并且该操作需要同步,这无法从并行处理中受益。

答案 2 :(得分:1)

我认为不应该提高性能。在生产者/消费者方案中,仅当您的消费者比生产者慢时才引入并发(对于消费者)才有意义。您应该期望通过引入多个使用者来获得那种性能提升。

但是这里的消费者已经比生产者(IO)快得多,因此没有性能提升。