sync.WaitGroup优于渠道的优势是什么?

时间:2016-03-17 09:37:10

标签: go concurrency channel

我正在使用并发Go库,我偶然发现了结果相似的goroutine之间的两种不同的同步模式:

使用Waitgroup

var wg sync.WaitGroup
func main() {
        words := []string{ "foo", "bar", "baz" }

        for _, word := range words {
                wg.Add(1)
                go func(word string) {
                        time.Sleep(1 * time.Second)
                        defer wg.Done()
                        fmt.Println(word)
                }(word)
        }
        // do concurrent things here

        // blocks/waits for waitgroup
        wg.Wait()
}

使用频道

func main() {
        words = []string{ "foo", "bar", "baz" }
        done := make(chan bool)
        defer close(done)
        for _, word := range words {
                go func(word string) {
                        time.Sleep(1 * time.Second)
                        fmt.Println(word)
                        done <- true
                }(word)
        }

        // Do concurrent things here

        // This blocks and waits for signal from channel
        <-done
}

我被告知sync.WaitGroup性能略高,我看到它被普遍使用。但是,我发现渠道更加惯用。使用sync.WaitGroup超过渠道的真正优势和/或什么样的情况可能会更好?

6 个答案:

答案 0 :(得分:29)

独立于第二个例子的正确性(正如评论中所解释的那样,你没有按照自己的想法行事,但它很容易修复),我倾向于认为第一个例子更容易理解。

现在,我甚至不会说渠道更加惯用。作为Go语言的标志性特征的频道不应该意味着尽可能使用它们是惯用的。 Go中惯用的是使用最简单易懂的解决方案:在这里,'#'.__mul__(10) = '##########' 传达意义(你的主要功能是WaitGroup为工人完成)和机制(工作人员在Wait)时会通知。

除非您处于非常具体的情况,否则我不建议您在此处使用频道解决方案。

答案 1 :(得分:2)

如果你对仅使用频道特别感兴趣,那么它需要以不同的方式完成(如果我们使用你的例子,就像@Not_a_Golfer指出的那样,它会产生不正确的结果)。

一种方法是创建int类型的通道。在工作进程中,每次完成作业时都会发送一个数字(这也可以是唯一的作业ID,如果您希望可以在接收器中跟踪它)。

在接收者主要例行程序中(将知道提交的作业的确切数量) - 在通道上进行范围循环,直到提交的作业数量没有完成,并且在所有作业完成后退出循环完成了。如果您想跟踪每个作业的完成情况(如果需要可能会做某些事情),这是一个很好的方法。

以下是供您参考的代码。减少totalJobsLeft将是安全的,因为它只会在通道的范围循环中完成!

//This is just an illustration of how to sync completion of multiple jobs using a channel
//A better way many a times might be to use wait groups

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {

    comChannel := make(chan int)
    words := []string{"foo", "bar", "baz"}

    totalJobsLeft := len(words)

    //We know how many jobs are being sent

    for j, word := range words {
        jobId := j + 1
        go func(word string, jobId int) {

            fmt.Println("Job ID:", jobId, "Word:", word)
            //Do some work here, maybe call functions that you need
            //For emulating this - Sleep for a random time upto 5 seconds
            randInt := rand.Intn(5)
            //fmt.Println("Got random number", randInt)
            time.Sleep(time.Duration(randInt) * time.Second)
            comChannel <- jobId
        }(word, jobId)
    }

    for j := range comChannel {
        fmt.Println("Got job ID", j)
        totalJobsLeft--
        fmt.Println("Total jobs left", totalJobsLeft)
        if totalJobsLeft == 0 {
            break
        }
    }
    fmt.Println("Closing communication channel. All jobs completed!")
    close(comChannel)

}

答案 2 :(得分:1)

这取决于用例。如果您要调度一次性作业以便并行运行而无需了解每项作业的结果,那么您可以使用WaitGroup。但是如果你需要从goroutines收集结果,那么你应该使用一个频道。

由于频道双向工作,我几乎总是使用频道。

另一方面,正如评论中指出的那样,您的频道示例未正确实施。您需要一个单独的通道来指示没有其他工作要做(一个例子是here)。在您的情况下,由于您事先知道了单词的数量,因此您可以使用一个缓冲通道并接收固定次数以避免声明关闭通道。

答案 3 :(得分:1)

我经常使用渠道从goroutine收集可能产生错误的错误消息。这是一个简单的示例:

func couldGoWrong() (err error) {
    errorChannel := make(chan error, 3)

    // start a go routine
    go func() (err error) {
        defer func() { errorChannel <- err }()

        for c := 0; c < 10; c++ {
            _, err = fmt.Println(c)
            if err != nil {
                return
            }
        }

        return
    }()

    // start another go routine
    go func() (err error) {
        defer func() { errorChannel <- err }()

        for c := 10; c < 100; c++ {
            _, err = fmt.Println(c)
            if err != nil {
                return
            }
        }

        return
    }()

    // start yet another go routine
    go func() (err error) {
        defer func() { errorChannel <- err }()

        for c := 100; c < 1000; c++ {
            _, err = fmt.Println(c)
            if err != nil {
                return
            }
        }

        return
    }()

    // synchronize go routines and collect errors here
    for c := 0; c < cap(errorChannel); c++ {
        err = <-errorChannel
        if err != nil {
            return
        }
    }

    return
}

答案 4 :(得分:1)

WaitGroup的主要优势是简单
通道可以是缓冲的,也可以是非缓冲的,并且可以承载一条消息或仅一个信号(没有消息-空通道),因此存在许多不同的用例,并且

“ WaitGroup等待goroutine的集合完成。 主要的goroutine调用Add来设置数量 等待的goroutines。然后每个goroutines 运行并在完成后调用完成。与此同时, 等待可以用来阻止,直到所有goroutine完成。”

让我们做一个基准:
TLDR:
the same code中使用sync.WaitGroup(与完成频道的方式相同)比缓冲完成频道(对于以下基准测试)快一点(与9%相比): br /> 695 ns/op758 ns/op
对于无缓冲完成的频道,使用sync.WaitGroup的速度更快(2x or more)-由于无缓冲的频道同步(对于以下基准测试):
722 ns/op2343 ns/op

基准(使用go version go1.14.7 linux/amd64

  1. 使用缓冲的已完成频道:
var done = make(chan struct{}, 1_000_000)

使用benchtime命令:

go test -benchtime=1000000x -benchmem -bench .
# BenchmarkEvenWaitgroup-8         1000000               695 ns/op               4 B/op          0 allocs/op
# BenchmarkEvenChannel-8           1000000               758 ns/op              50 B/op          0 allocs/op

  1. 使用未缓冲的已完成频道:
var done = make(chan struct{})

使用此命令:

go test -benchtime=1000000x -benchmem -bench .
# BenchmarkEvenWaitgroup-8         1000000               722 ns/op               4 B/op          0 allocs/op
# BenchmarkEvenChannel-8           1000000              2343 ns/op             520 B/op          1 allocs/op

Code

package main

import (
    "sync"
)

func main() {
    evenWaitgroup(8)
}

func waitgroup(n int) {
    select {
    case ch <- n: // tx if channel is empty
    case i := <-ch: // rx if channel is not empty
        // fmt.Println(n, i)
        _ = i
    }
    wg.Done()
}

func evenWaitgroup(n int) {
    if n%2 == 1 { // must be even
        n++
    }
    for i := 0; i < n; i++ {
        wg.Add(1)
        go waitgroup(i)
    }
    wg.Wait()
}

func channel(n int) {
    select {
    case ch <- n: // tx if channel is empty
    case i := <-ch: // rx if channel is not empty
        // fmt.Println(n, i)
        _ = i
    }
    done <- struct{}{}
}

func evenChannel(n int) {
    if n%2 == 1 { // must be even
        n++
    }
    for i := 0; i < n; i++ {
        go channel(i)
    }
    for i := 0; i < n; i++ {
        <-done
    }
}

var wg sync.WaitGroup
var ch = make(chan int)
var done = make(chan struct{}, 1000000)

// var done = make(chan struct{})

注意:为已缓冲和未缓冲的已完成频道基准测试切换注释:

var done = make(chan struct{}, 1000000)
// var done = make(chan struct{})

main_test.go文件:

package main

import (
    "testing"
)

func BenchmarkEvenWaitgroup(b *testing.B) {
    evenWaitgroup(b.N)
}
func BenchmarkEvenChannel(b *testing.B) {
    evenChannel(b.N)
}

答案 5 :(得分:0)

同时建议使用waitgroup,但仍然想用通道进行,然后在下面我提到一个简单的使用频道

0