“粉丝” - 一个“扇出”的行为

时间:2015-04-23 15:26:09

标签: go

说,我们有三种方法来实现“扇入”行为

func MakeChannel(tries int) chan int {
    ch := make(chan int)

    go func() {
        for i := 0; i < tries; i++ {
            ch <- i
        }
        close(ch)
    }()

    return ch
}

func MergeByReflection(channels ...chan int) chan int {
    length := len(channels)
    out := make(chan int)
    cases := make([]reflect.SelectCase, length)
    for i, ch := range channels {
        cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
    }
    go func() {
        for length > 0 {
            i, line, opened := reflect.Select(cases)
            if !opened {
                cases[i].Chan = reflect.ValueOf(nil)
                length -= 1
            } else {
                out <- int(line.Int())
            }
        }
        close(out)
    }()
    return out
}

func MergeByCode(channels ...chan int) chan int {
    length := len(channels)
    out := make(chan int)
    go func() {
        var i int
        var ok bool

        for length > 0 {
            select {
            case i, ok = <-channels[0]:
                out <- i
                if !ok {
                    channels[0] = nil
                    length -= 1
                }
            case i, ok = <-channels[1]:
                out <- i
                if !ok {
                    channels[1] = nil
                    length -= 1
                }
            case i, ok = <-channels[2]:
                out <- i
                if !ok {
                    channels[2] = nil
                    length -= 1
                }
            case i, ok = <-channels[3]:
                out <- i
                if !ok {
                    channels[3] = nil
                    length -= 1
                }
            case i, ok = <-channels[4]:
                out <- i
                if !ok {
                    channels[4] = nil
                    length -= 1
                }
            }
        }
        close(out)
    }()
    return out
}

func MergeByGoRoutines(channels ...chan int) chan int {
    var group sync.WaitGroup

    out := make(chan int)
    for _, ch := range channels {
        go func(ch chan int) {
            for i := range ch {
                out <- i
            }
            group.Done()
        }(ch)
    }
    group.Add(len(channels))
    go func() {
        group.Wait()
        close(out)
    }()
    return out
}

type MergeFn func(...chan int) chan int

func main() {
    length := 5
    tries := 1000000
    channels := make([]chan int, length)
    fns := []MergeFn{MergeByReflection, MergeByCode, MergeByGoRoutines}

    for _, fn := range fns {
        sum := 0
        t := time.Now()
        for i := 0; i < length; i++ {
            channels[i] = MakeChannel(tries)
        }
        for i := range fn(channels...) {
            sum += i
        }
        fmt.Println(time.Since(t))
        fmt.Println(sum)
    }
}

结果是(在1 CPU,我使用了runtime.GOMAXPROCS(1)):
19.869s(MergeByReflection)
2499997500000个
8.483s(MergeByCode)
2499997500000个
4.977s(MergeByGoRoutines)
2499997500000

结果是(在2 CPU,我使用了runtime.GOMAXPROCS(2)):
44.94s
2499997500000个
10.853s
2499997500000个
3.728s
2499997500000

  • 我理解MergeByReflection最慢的原因,但是MergeByCode和MergeByGoRoutines之间的区别是什么?
  • 当我们增加CPU数量时,为什么“select”子句(直接使用MergeByReflection和间接使用MergeByCode)会变慢?

1 个答案:

答案 0 :(得分:3)

这是一个初步评论。示例中的通道都是无缓冲的,这意味着它们可能会在放置或获取时间时阻塞。

在此示例中,除了频道管理之外几乎没有其他处理。因此,性能由同步原语支配。实际上,这些代码很少可以并行化。

在MergeByReflection和MergeByCode函数中,select用于侦听多个输入通道,但没有采取任何措施来考虑输出通道(因此可能阻塞,而某些事件可能在其中一个输入通道上可用) 。

在MergeByGoRoutines函数中,不会发生这种情况:当输出通道阻塞时,它不会阻止另一个输入通道被另一个goroutine读取。因此,运行时有更好的机会来并行化goroutine,并减少对输入通道的争用。

MergeByReflection代码是最慢的,因为它有反射的开销,几乎没有任何东西可以并行化。

MergeByGoRoutines函数是最快的,因为它减少了争用(需要更少的同步),并且因为输出争用对输入性能的影响较小。因此,当使用多个内核运行时,它可以带来很小的改进(与其他两种方法相反)。

MergeByReflection和MergeByCode有很多同步活动,在多个核心上运行会对性能产生负面影响。但是,通过使用缓冲通道可以获得不同的性能。