• Goroutine之Channel


    一、channel基础

    1、引入

    在Goroutine基础中我们通过WaitGroup解决了主线程因为无法感知其它协程是否结束而造成提前结束的问题,通过锁机制解决了多协程之间共享数据而造成数据混乱和安全的问题。归结起来协程问题:

    • 资源竞争,数据共享而引发数据安全问题
    • 一个协程不知道另一个协程什么时候结束

    那么通过之前的方式解决这些问题,还可以通过更高级一点的手段,那就是channel管道。

    channel的本质是一个队列(先进先出)的数据结构,多goroutine访问时不需要加锁,本身就是安全的,channel中的元素时有类型的,int类型的只能放int类型。

     2、语法

    var 变量名 chan 数据类型

    例如:

    var intChan chan int // intChan中存放的是int类型
    var mapChan chan map[int]string// mapChan 中存放的是map[int]string类型
    var userChan chan User//Person结构体类型
    var userChan chan *User//Person指针类型
    ...
     

    注意:

    • channel是引用类型,所以传入函数中的channel是同一个channel
    • channel必须初始化,即make后使用
    • channel是有类型的
    package main
    
    import "fmt"
    
    func main() {
        // 创建一个可以存放5个int类型的管道
        var intChan chan int = make(chan int, 5)
    
        // intChan是一个引用类型
        fmt.Printf("intChan的值=%v, intChan本身的地址=%p \n", intChan, &intChan) // intChan的值=0xc000020090, intChan本身的地址=0xc000006028
    
        // 向管道中写入数据,写入的数据不能超过容量
        intChan <- 20
        intChan <- 10
    
        // 查看管道的长度和容量(cap)
        fmt.Printf("len=%v, cap=%v \n", len(intChan), cap(intChan)) // len=2, cap=5
    
        // 读取管道中的值
        var num1 int
        num1 = <-intChan
        fmt.Println(num1) // 20
    
        //再次读取长度和容量
        fmt.Printf("len=%v, cap=%v \n", len(intChan), cap(intChan)) // len=1, cap=5
    }

    可以看到管道中一边写入数据,一边取出数据,但是如果管道中的数据取完了,还继续取出就会出现deadlock思索问题。 

    3、channel的遍历与关闭

    使用内置函数close可以关闭channel,当channel关闭后就不能向channel写数据了,但是仍然可以从该channel中读取数据。 

     channel支持for-range的方式遍历,注意的是:

    • 在遍历时,如果channel没有关闭,则会出现deadlock错误
    • 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完毕后就会退出遍历
    package main
    
    import "fmt"
    
    func main() {
    
        var intChan chan int = make(chan int, 1000)
    
        for i := 0; i < 800; i++ {
    
            intChan <- i // 管道中放入800个数
    
        }
        // 放入数字后,关闭管道,循环结束不会报deadlock问题
        close(intChan)
    
        // 遍历管道使用for-range, 不能使用普通循环
        for v := range intChan {
            fmt.Println("v=", v)
        }
    
    }

    二、管道阻塞

    channel的用法一般时既有写又有读,类似于生产者、消费者模型,但是如果只有生产者,而没有消费者就会出现阻塞。比如,管道的容量是10,但是你如果向里面放入30个数就会出现阻塞,从而导致deadlock的产生。比如:

    package main
    
    func main() {
    
        var numChan chan int = make(chan int, 10)
    
        for i := 0; i < 30; i++ {
    
            numChan <- i
    
        }
    
    }
    /*
    fatal error: all goroutines are asleep - deadlock!
    */

    那么如何解决这个问题呢?那么就需要避免阻塞,读写频率可以不同但是必须两方都存在。如:

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var wg sync.WaitGroup
    var numChan chan int = make(chan int, 1000)
    
    func write(numChan chan int) {
        defer wg.Done()
        for i := 0; i < 500; i++ {
            numChan <- i
        }
        close(numChan) //写入完毕后一定要关闭管道,否则读取到最后会deadlock
    
    }
    
    func read(numChan chan int) {
        defer wg.Done()
    
        for {
            v, flag := <-numChan
            if !flag {
                break
            }
            fmt.Println(v)
            time.Sleep(time.Second)
        }
    
    }
    
    func main() {
        wg.Add(2)
        // 启动2个协程
        go write(numChan)
        go read(numChan)
    
        wg.Wait()
    
    }

    可以看到上面的写的协程速度很快,但是读的很慢,但是golang的机制会检测出有人消费就不会出现deadlock。

    三、select

      select语句可以选择一组可能的send操作或者receive操作,类似switch,但是只是用来处理通讯操作。case后的语句可以是send语句,也可以是receive语句,亦或者是default语句。最多同时允许一个case语句执行,如果所有的case都没有匹配上就会执行default语句。那么使用它可以处理什么问题呢?解决未关闭管道channel造成阻塞而出现deadlock问题。

    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    var wg sync.WaitGroup
    var numChan chan int = make(chan int, 1000)
    
    func write(numChan chan int) {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            numChan <- i
        }
        //close(numChan) //下面通过select解决了如果不关闭管道出现阻塞而产生deadlock问题
    
    }
    
    func read(numChan chan int) {
        defer wg.Done()
    
        for {
            // 有时并不清楚何时关闭管道,通过select解决未关闭管道出现deadlock问题
            select {
            case v := <-numChan:
                fmt.Println(v)
                time.Sleep(time.Second)
    
            default:
                fmt.Println("读取结束")
                return
    
            }
    
        }
    
    }
    
    func main() {
        wg.Add(2)
        // 启动2个协程
        go write(numChan)
        go read(numChan)
    
        wg.Wait()
    
    }

     有时我们并不清楚何时关闭管道,所以通过select方式可以在写完数据不关闭管道避免出现deadlock问题。

    四、单向管道

     管道在默认情况下是双向的,但是它可以声明为只读或者只写:

    // 只读声明
    var numChan <-chan int
    
    // 只写声明
    var numChan chan<-  int

    例如:

    package main
    
    import "fmt"
    
    func main() {
    
        var numChan chan<- int // 声明一个只写管道
    
        numChan <- 1
    
        var numChan1 <-chan int // 声明一个只读管道
        var num1 = <-numChan1
        fmt.Println(num1)
    
    }

    应用场景:

    package main
    
    import (
        "fmt"
        "sync"
    )
    
    var wg sync.WaitGroup
    
    func send(numChan chan<- int) {
        // chan<- 只写
        defer wg.Done()
        for i := 0; i < 20; i++ {
            numChan <- i
        }
        close(numChan)
    
    }
    
    func recv(numChan <-chan int) {
        // <-chan 只读
        defer wg.Done()
        for {
            v, flag := <-numChan
            if !flag {
                break
            }
            fmt.Println(v)
        }
    }
    
    func main() {
    
        var numChan chan int = make(chan int, 20)
        wg.Add(2)
    
        go send(numChan)
        go recv(numChan)
    
        wg.Wait()
    
    }

    五、实战演练

    完成goroutine和channel的协同工作:

    • 开启一个writeData协程,向管道intChan写入50个数
    • 开启一个readData协程,从管道intChan中读出数据

    注意:上述两个协程操作的是同一个管道,主协程需要等上述两个协程都完成工作才能退出

    思路分析:

     代码实现:

    package main
    
    import "fmt"
    
    func writeData(intChan chan int) {
        for i := 0; i < 50; i++ {
            intChan <- i
        }
        defer close(intChan)
    }
    
    func readData(intChan chan int, exitChan chan bool) {
        for {
            v, flag := <-intChan
            if !flag {
                break
            }
            fmt.Println(v)
        }
        // 完成读取协程,向exitChan发送信息
        exitChan <- true
        // 关闭管道
        defer close(exitChan)
    }
    
    func main() {
        var intChan chan int = make(chan int, 50)
        var exitChan chan bool = make(chan bool, 1)
    
        go writeData(intChan)
        go readData(intChan, exitChan)
    
        for {
            _, flag := <-exitChan
            if flag {
                // 说明读取协程结束,可以结束主协程了
                break
            }
        }
    
    }
  • 相关阅读:
    Integer判等的陷阱:你知道Integer内部高速缓冲区IntegerCache吗?
    Unicode 是不是只有两个字节,为什么能表示超过 65536 个字符
    java中Char到底是什么格式的编码
    Java中char和String 的深入理解
    关于serialVersionUID的说明
    Java中的instanceof和isInstance基础讲解
    编码(1)学点编码知识又不会死:Unicode的流言终结者和编码大揭秘
    知识图谱和图卷积(贪心学院)——学习笔记
    索尼相机,索尼W35,Sony Cyber-shot DSC-w35
    高斯分布与高斯过程梳理
  • 原文地址:https://www.cnblogs.com/shenjianping/p/15858240.html
Copyright © 2020-2023  润新知