美文网首页
2018-11-06 golang 中Channel用法笔记

2018-11-06 golang 中Channel用法笔记

作者: Amberlucky | 来源:发表于2018-11-06 14:32 被阅读0次

    本来觉得自己的golang的channel已经有所了解了,但实际在被问一些问题的时候,还是没法回答的非常明确,所以今天又梳理整理了一遍,并且把容易出错的问题记录一下。

    废话不多说,先上一段代码:

    package main
    
    import (
        "fmt"
    )
    
    func Count(ch chan int, n int) {
        fmt.Println("Counting ", n)
        ch <- n
    }
    
    func main() {
        chs := make([]chan int, 10)
        for i := 0; i < 10; i++ {
            chs[i] = make(chan int)
            go Count(chs[i], i)
        }
    
        for _, ch := range chs {
            v := <-ch
            fmt.Println("v  ", v)
        }
    }
    

    想一下输出结果应该是什么呢?我承认这个一开始确实自己没有看仔细,chs是一个信道数组,他的每一个参数都是重新new的一个信道,所以输出结果应该是这样:

    Counting  9
    Counting  0
    v   0
    Counting  1
    v   1
    Counting  6
    ... 
    全部会输出,并且顺序不一定
    

    我一开始想,这个信道不是没有缓存吗?应该读一个取一个才对,但是没看到,其实数组里面的每一个参数都是重新new的信道。那什么情况才会是我想象中的输出结果呢?代码可以改成这样,只改main函数就可以:

    func main() {
        ch := make(chan int)
        for i := 0; i < 10; i++ {
            go Count(ch, i)
        }
    
        for i := 0; i < 10; i++ {
            v := <-ch
            fmt.Println("v  ", v)
        }
    }
    

    这个时候,就是读一个取一个,因为只有一个信道,输出结果:

    Counting  9
    v   9
    Counting  0
    v   0
    Counting  1
    v   1
    Counting  2
    v   2
    ...
    

    这个时候,我想用一个带缓存的信道进行存储,然后用range进行读取,代码就改成了这样:

    func Count(ch chan int, n int) {
        fmt.Println("Counting ", n)
        ch <- n
    
    }
    
    func main() {
        ch := make(chan int, 10) //改成有缓存的信道
        for i := 0; i < 10; i++ {
            go Count(ch, i)
        }
    
        for v := range ch {
            fmt.Println("v  ", v)
        }
    }
    

    结果输出的时候报错了:

    ...
    Counting  8
    v   8
    fatal error: all goroutines are asleep - deadlock!
    
    goroutine 1 [chan receive]:
    main.main()
            /home/tfd/workspace/go/src/test/main.go:24 +0x126
    

    死锁了,原来是因为range不会检测信道是否干涸(drained,也就是信道是否已经读取完),在读取完所有数据后,再次读取,会导致main函数挂起。那怎么办呢,第一种方法,如果我知道一共缓存了多少数据,那我就读多少数据就可以了,代码如下:

    func main() {
        ch := make(chan int, 10)
        for i := 0; i < 10; i++ {
            go Count(ch, i)
        }
    
        for i := 0; i < 10; i++ { //这边假设我知道一共存了10个数据,那就读10次就好了,如果读11次,那就会报deadlock错误
            v := <-ch
            fmt.Println("v  ", v)
        }
    }
    

    读取结果如下:

    Counting  9
    v   9
    Counting  0
    v   0
    Counting  1
    v   1
    Counting  2
    v   2
    Counting  3
    v   3
    Counting  7
    v   7
    Counting  6
    v   6
    Counting  8
    v   8
    Counting  4
    Counting  5
    v   4
    v   5
    

    那实际现实中,我很大可能不知道一共写入了多少次,那怎么办呢?我们可以用sync.WaitGroup, 简单介绍下WaitGroup对象内部有一个计数器,最初从0开始,它一共包含三个方法:Add(),Done(),Wait()用来控制计数器的数量。Add(n)把计数器设置为nDone()每次把计数器-1Wait()会阻塞代码的运行,直到计数器的值减为0. 那我们用WaitGroup改一下代码就可以了。

    func Count(ch chan int, n int, wg *sync.WaitGroup) {
        defer wg.Done()
        fmt.Println("Counting ", n)
        ch <- n
    }
    
    func main() {
        //wg := sync.WaitGroup{} //这里可以用new(sync.WaitGroup),这样直接就可以得到指针类型的WaitGroup
          wg:= new(sync.WaitGroup)
            ch := make(chan int, 10)
    
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go Count(ch, i, wg)
        }
    
        go func() {
            wg.Wait()
            close(ch)
            fmt.Println("Finish")
        }()
    
        for v := range ch {
            fmt.Println("v:", v)
        }
    }
    

    这时,代码运行就没问题了,输出结果

    Counting  5
    Counting  4
    v: 5
    v: 4
    Counting  2
    v: 2
    Counting  6
    Counting  1
    v: 6
    Counting  0
    Counting  8
    Counting  3
    v: 1
    v: 0
    v: 8
    v: 3
    Counting  7
    v: 7
    Counting  9
    Finish
    v: 9
    

    但是在这里我也是遇到坑的,一开始WaitGroup没有传指针,代码是这样的:

    func Count(ch chan int, n int, wg sync.WaitGroup) { //wg没有传指针
        defer wg.Done()
        fmt.Println("Counting ", n)
        ch <- n
    }
    
    func main() {
        wg := sync.WaitGroup{}
        ch := make(chan int, 10)
    
        for i := 0; i < 10; i++ {
            wg.Add(1)
            go Count(ch, i, wg) //wg没有传指针
        }
    
        go func() {
            wg.Wait()
            close(ch)
            fmt.Println("Finish")
        }()
    
        for v := range ch {
            fmt.Println("v:", v)
        }
    }
    
    

    这个时候,代码仍然报deadlock,结果我就不贴了。这是因为这个时候wg是将拷贝传递到了goroutine中,所以,wg只有Add操作,而Done操作是在wg的副本中执行的,因此Wait就死锁了。正确的应该是要传指针,上一段代码已经把正确的写进去了。

    这里还可以注意一点就是,信道close了之后可以读但是不可以写,并且,close之后的信道是不会阻塞的,所以才可以通过WaitGroup的方法在数据写完之后把信道close就不会出现阻塞的情况了。

    相关文章

      网友评论

          本文标题:2018-11-06 golang 中Channel用法笔记

          本文链接:https://www.haomeiwen.com/subject/ycayxqtx.html