美文网首页go
22-Go语言管道(Channel)

22-Go语言管道(Channel)

作者: 喝酸奶要舔盖__ | 来源:发表于2018-10-14 10:50 被阅读0次

    多线程同步问题

    • 互斥锁
      • 互斥锁的本质是当一个goroutine访问的时候, 其它goroutine都不能访问
      • 这样就能实现资源同步, 但是在避免资源竞争的同时也降低了程序的并发
        性能,程序由原来的并发执行变成了串行
    • 打印案例
      • 没有添加互斥锁, 那么两个人都有机会输出自己的内容
      • 添加互斥锁,只有当一个输出完毕的时候,另一个才能输出
    package main
    
    import (
        "fmt"
        "sync"
        "time"
    )
    
    //创建一个互斥锁
    //可以让程序从并发状态变成并行状态
    var lock = sync.Mutex{}
    
    //定义一个打印字符的函数
    func myprint(str string)  {
        //添加锁
        lock.Lock()
        for _, value := range str {
            time.Sleep(time.Microsecond *300)
            fmt.Printf("%c", value)
        }
    
        //解锁
        lock.Unlock()
    }
    
    //调用者1
    func person1()  {
        myprint("hello")
    }
    
    //调用者2
    func person2()  {
        myprint("world")
    }
    func main() {
        //开启go程
        go person1()
        go person2()
    
        //保证主线程不退出,程序不结束
        for  {
            ;
        }
    }
    

    生产者与消费者

    • 生产者消费者模型
      • 某个模块(函数)负责生产数据, 这些数据由另一个模块来负责处理
      • 一般生产者消费者模型包含三个部分生产者缓冲区消费者
      • 没有缓冲区,消费者发生变化, 会直接影响生产者, 耦合性太强
      • 添加缓冲区可以提高效率

    生产者和消费者资源竞争问题

    • 生产者生产产比较慢, 而消费比较快, 就会导致消费者消费到错误数据
    package main
    
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    //定义数组模拟缓冲区
    var arr [10]int
    
    //定义模拟生产者函数
    func producter()  {
        //定义随机因子
        rand.Seed(time.Now().UnixNano())
        //产生随机数
        for i := 0;i < 10 ;i++  {
            num := rand.Intn(100)
            fmt.Println("生产者生产了", num)
            //将生产的数据放入缓冲区中
            arr[i] = num
            time.Sleep(time.Millisecond * 300)
    
        }
    }
    
    //定义函数模拟消费者
    func consumer(){
        for i := 0;i < 10 ;i++  {
            value := arr[i]
            fmt.Println("------消费者消费了",value)
        }
    }
    
    func main() {
        // 我们想要的是, 只有生产者生产了, 我们才能消费
        // 注意点: 在多go程中, 如果生产者生产的太慢, 那么消费者就会消费到错误的数据
        go producter()
        // 注意点: 看上去通过给生产者以及消费者同时加锁就能解决, 只有生产完了才能消费
        //         但是取决于谁想执行加锁操作, 所以不完美
        go consumer()
    
        for  {
            ;
        }
    }
    
    • 利用互斥锁解决问题
    package main
    
    import (
        "fmt"
        "math/rand"
        "sync"
        "time"
    )
    //创建一把互斥锁
    var lock = sync.Mutex{}
    
    //定义数组模拟缓冲区
    var arr [10]int
    
    //定义模拟生产者函数
    func producter()  {
        /*
        为什么在生产者和消费者中都上锁之后, 就可以实现生产完再消费?
        因为生产者和消费者中的锁都是同一把, 都是全局变量lock
    
        调用Lock()函数的作用: 是修改Mutex结构体中的state属性的值, 将它改为一个非0的值
        每次上锁的时候都会判断有没有被锁定, 如果已经锁定就不锁了, 并且不会执行后面的代码
        调用Unlock函数的作用: 是修改Mutex结构体中的state属性的值, 将它改为0
         */
    
        //上锁
        // 这里锁定的是当前go程, 也就是当前函数
        // 意味着其它的go程不能执行当前的函数, 只有当前锁定这个函数的go程才能执行这个函数
        lock.Lock()
        //定义随机因子
        rand.Seed(time.Now().UnixNano())
        //产生随机数
        for i := 0;i < 10 ;i++  {
            num := rand.Intn(100)
            fmt.Println("生产者生产了", num)
            //将生产的数据放入缓冲区中
            arr[i] = num
            //time.Sleep(time.Millisecond * 300)
        }
        //解锁
        lock.Unlock()
    }
    
    //定义函数模拟消费者
    func consumer(){
        //上锁
        lock.Lock()
        for i := 0;i < 10 ;i++  {
            value := arr[i]
            fmt.Println("------消费者消费了",value)
        }
    
        //解锁
        lock.Unlock()
    }
    
    func main() {
        // 我们想要的是, 只有生产者生产了, 我们才能消费
        // 注意点: 在多go程中, 如果生产者生产的太慢, 那么消费者就会消费到错误的数据
        go producter()
        // 注意点: 看上去通过给生产者以及消费者同时加锁就能解决, 只有生产完了才能消费
        //         但是取决于谁想执行加锁操作, 所以不完美
        go consumer()
    
        for  {
            ;
        }
    }
    

    Go语言管道

    管道的基本使用
    • Channel的本质是一个队列
    • Channel是线程安全的, 也就是自带锁定功能
    • Channel声明和初始化
      • var 变量名称 chan 数据类型
      • make(chan 数据类型, 容量)
      • 管道和切片/字典一样,必须创建后才能使用,否则会报错
      • Channel和切片还有字典一样, 是引用类型,是地址传递
    package main
    
    import "fmt"
    
    func main() {
        /*
        1.什么是管道:
        管道就是一个队列, 具备先进先出的原则
        是线程安全的, 也就是自带锁定功能
    
        2.管道作用:
        在Go语言的协程中, 一般都使用管道来保证多个协程的同步, 或者多个协程之间的通讯
    
        3.如何声明一个管道, 和如何创建一个管道
        管道在Go语言中和切片/字典一样也是一种数据类型
        管道和切片/字典非常相似, 都可以用来存储数据, 都需要make之后才能使用
        3.1管道声明格式:
        var 变量名称 chan 数据类型
        var myCh chan int
        如上代码的含义: 声明一个名称叫做myCh的管道变量, 管道中可以存储int类型的数据
        3.2管道的创建:
        make(chan 数据类型, 容量)
        myCh = make(chan int, 3);
        路上代码的含义: 创建一个容量为3, 并且可以保存int类型数据的管道
    
        4.管道的使用
        4.1如何往管道中存储(写入)数据?
        myCh<-被写入的数据
        4.2如何从管道中获取(读取)数据?
        <-myCh
        对管道的操作是IO操作
        例如: 过去的往文件中写入或者读取数据, 也是IO操作
        例如: 过去的往屏幕上输出内容, 或者从屏幕获取内容, 也是IO操作
        stdin / stdout / stderr
    
        注意点:
        和切片不同, 在切片中make函数的第二个参数表示的切片的长度(已经存储了多少个数据),
        而第三个参数才是指定切片的容量
        但是在管道中, make函数的第二个参数就是指定管道的容量, 默认长度就是0
         */
    
         //1.定义一个管道
         //var myChan chan int
         //2.使用make创建管道
         myChan := make(chan int, 3)
         //3.往管道中存储数据
         myChan<-1
         myChan<-2
         myChan<-3
         //从管道中取出数据
         fmt.Println(<-myChan)
         fmt.Println(<-myChan)
         fmt.Println(<-myChan)
         
    
        //定义一个管道
        var myChan chan int
        //直接使用管道
        //注意点: 会报错,管道定义完成后不创建是无法直接使用的
        //myChan<-666
        //fmt.Println(<-myChan)
    
        //创建管道
        myChan = make(chan int, 3)
        //只要往管道中写入了数据, 那么len就会增加
        myChan <- 2
        //fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
        myChan <- 4
        //fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
        myChan <- 6
        //fmt.Println("len = ", len(myChan), "cap = ", cap(myChan))
        //注意点: 如果len等于cap, 那么就不能往管道中再写入数据了, 否则会报错
        //myChan <- 8
    
        //管道未写入数据,使用管道去取数据会报错
        //从管道中取数据,len会减少
        //<-myChan
        fmt.Println(<-myChan)
        fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
        fmt.Println(<-myChan)
        fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
        fmt.Println(<-myChan)
        fmt.Println("len=",len(myChan),"cap = ", cap(myChan))
        //注意点: 取数据个数也不可以超出写入的数据个数,否则会报错
        //fmt.Println(<-myChan)
    }
    

    管道的遍历和关闭
    • 管道遍历推荐两种方式
      • for..range方法遍历
      • for死循环遍历
    package main
    
    import "fmt"
    
    func main() {
        /*
        管道的遍历:
        可以使用for循环, 也可以使用 for range循环, 以及死循环来遍历
        但是更推荐使用后两者
    
        因为在企业开发中, 有可能我们不知道管道中具体有多少条数据, 所以如果利用for循环来遍历, 那么无法确定遍历的次数, 并且如果遍历的次数太多, 还会报错
         */
    
        //创建一个管道
        myChan := make(chan int, 3)
        //往管道中写入数据
        myChan <- 2
        myChan <- 4
        myChan <- 6
        //注意点: 如果不关闭管道,遍历管道会报错
        close(myChan)
        //第一种方法遍历管道
        //for value := range myChan {
        //  fmt.Println(value)
        //}
    
        //第二种方法遍历管道
        //注意点: 如果被遍历的管道没有关闭, 那么会报错
        //        如果管道没有被关闭, 那么会将true返回给ok, 否则会将false返回给Ok
        for {
            if v,ok := <-myChan; ok {
                fmt.Println(v)
                fmt.Println(ok)
            }else {
                break
            }
        }
    
    
        //注意点: 管道关闭后无法往里面写入数据,会报错,但是可以读取数据不会报错
        myChan := make(chan int,3)
        close(myChan)
        //往管道中写入数据
        //myChan<-1
        //myChan<-2
        //myChan<-3
        //从管道中读取数据
        <-myChan
    }
    

    管道阻塞现象
    • 阻塞现象只会发生在go程中运行才会发生,在普通线程中不会发生这种现象
    package main
    
    import "fmt"
    
    //定义一个管道
    var myChan = make(chan int,2)
    
    func test()  {
        /*myChan<-1
        myChan<-2
        fmt.Println("管道满之前的代码")
        //这里不会报错, 会阻塞, 等到将管道中的数据读出去之后, 有的新的空间再往管道中写
        myChan<-3
        fmt.Println("管道满之后的代码")*/
        //这里不会报错,会阻塞,等到有人往管道中写入数据之后,有新的数据之后才会读取
        fmt.Println("读取之前的代码")
        <-myChan
        fmt.Println("读取之后的代码")
    }
    
    func main() {
        /*
        单独在主线程中操作管道, 写满了会报错, 没有数据去读取也会报错
        只要在go程中操作管道, 无论有没有写满, 无论有没有数据都会发生管道阻塞的现象
        */
        go test()
        for  {
            ;
        }
    }
    
    • 利用管道阻塞实现并发变串行
    package main
    
    import (
        "fmt"
        "time"
    )
    //创建一个管道
    var myChan = make(chan bool)
    
    
    func printer(str string)  {
        for _, value := range str {
            fmt.Printf("%c", value)
            time.Sleep(time.Microsecond * 300)
        }
    }
    
    func person1()  {
        printer("hello")
        //往管道中写入数据
        //只有printer函数执行完,才会往管道中写入数据
        myChan<-true
    }
    
    func person2()  {
        //从管道中读取数据
        //只有管道中有数据才会读取,否则会阻塞
        <-myChan
        printer("world")
    }
    
    func main() {
        go person1()
        go person2()
        for  {
            ;
        }
    }
    
    • 利用管道阻塞实现生产者和消费者模型
    package main
    
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    //定义管道模拟缓冲区
    var myChan = make(chan int, 10)
    
    //定义生产者函数
    func producter() {
        //定义随机因子
        rand.Seed(time.Now().UnixNano())
        //生成随机数
        for i := 0; i < 10; i++ {
            num := rand.Intn(100)
            fmt.Println("生产者生产了", num)
            //将生产的数据存入到管道中
            myChan <- num
        }
    }
    
    //定义函数模拟消费者
    func customer() {
        //从管道中读取数据
        for i := 0; i < 10; i++ {
            num := <-myChan
            fmt.Println("----消费者消费了", num)
        }
    
    }
    func main() {
        //创建两个go程
        //多个生产者和多个消费者
        go producter()
        go producter()
    
    
        go customer()
        go customer()
    
        for {
            ;
        }
    }
    

    无缓冲区管道
    • 管道容量为0的管道就是无缓冲区管道
    package main
    
    import "fmt"
    
    func main() {
        /*
        // 管道总结:
        // 管道一般都在go程中使用, 不会直接在主线程中使用, 无论是有缓冲的还是没有缓冲的
        // 只要是在go程中使用, 无论是有缓冲的还是没有缓冲的, 都会出现阻塞现象
    
        */
        //创建无缓冲管道
        // 注意点:
        // 没有缓冲的管道不能直接存储数据
        // 没有缓冲的管道不能直接获取数据
        // 注意点:
        // 想使用没有缓冲的管道, 必须保证读和写同时存在, 而且还必须保证读和写是在不同的go程中,至少有一个在go程中
        // 并且读必须写在写的前面
        //myChan := make(chan int,0)
        // 没有缓冲的管道不能直接获取数据
        //fmt.Println(<-myChan)
        // 没有缓冲的管道不能直接存储数据
        //myChan<-1
    
        /*//读取管道在go程中
        go func() {
            fmt.Println("读之前的代码")
            fmt.Println(<-myChan)
            fmt.Println("读之前的代码")
    
        }()
    
        //写入管道在主线程中
        func(){
            time.Sleep(time.Second * 5)
            fmt.Println("写之前的代码")
            myChan<-2
            fmt.Println("写之后的代码")
        }()*/
    
    
        //无缓冲管道单个使用
        //如果是在go程中使用无缓冲的管道, 那么就可以单个使用(只有读, 或者只有写)
        myChan := make(chan int,0)
        go func() {
            //只读不会报错
            //<-myChan
            //只写不会报错
            fmt.Println("写之前的代码")
            myChan<-1
            fmt.Println("写之后的代码")
        }()
    
        for  {
            ;
        }
    }
    
    • 利用无缓冲区管道解决主线程结束问题
    package main
    
    import "fmt"
    
    func main() {
        //创建一个有缓冲管道
        myChan := make(chan int,3)
        //创建一个无缓冲管道
        exitChan := make(chan bool,0)
    
        //往管道中存储数据
        go func() {
            for i := 0; i < 3; i++ {
                myChan<-i
                fmt.Println("生产了",i)
            }
            //无缓冲管道存储数据
            exitChan<-true
        }()
    
        <-exitChan
        //for  {
        //  ;
        //}
    }
    

    单向管道
    • 默认情况下所有的管道都是双向的管道(可读可写),那么在企业开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,,那么这个时候我们就可能会使用单向管道
    • 双向管道格式
      • var myCh chan int
    • 单向管道格式
      • var myCh chan<- int; 只写的管道
      • var myCh <-chan int; 只读的管道
    • 注意点:
      • 双向管道可以转换为单向的管道
      • 但是单向的管道不能转换为双向的管道
    package main
    
    import "fmt"
    
    func main() {
        /*
        默认情况下所有的管道都是双向的管道(可读可写)
        那么在企业开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,
        那么这个时候我们就可能会使用单向管道
    
        双向格式:
        var myCh chan int;
        myCh = make(chan int, 5)
        myCh = make(chan int)
    
        单向格式:
        var myCh chan<- int; 只写的管道
        var myCh <-chan int; 只读的管道
    
        双向管道可以转换为单向的管道
        但是单向的管道不能转换为双向的管道
        */
    
        //定义一个双向管道
        myChan := make(chan int,3)
        //定义一个只读的单向管道
        var myChan1 <-chan int
        //定义一个只写的单向管道
        //var myChan2 chan<- int
    
        /*//将双向管道赋给只读的单向管道
        myChan1 = myChan
        fmt.Println(myChan1)
    
        //将双向管道赋给只写的单向管道
        myChan2 = myChan
        fmt.Println(myChan2)*/
    
        //将单向管道赋给双向管道
        //会报错
        //myChan = myChan1
        //fmt.Println(myChan)
    }
    
    • 单向管道作为函数参数
    package main
    
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    //定义一个生产者
    func producter(buff chan <- int)  {
        //定义随机因子
        rand.Seed(time.Now().UnixNano())
        //生成随机数
        for i := 0;i < 5 ;i++  {
            num := rand.Intn(100)
            //将产生的随机数存储到管道中
            buff <- num
            fmt.Println("生产者生产了", num)
        }
    }
    
    //定义消费者函数
    func consumer(buff <- chan  int, exitChan chan <- int )  {
        for i := 0;i < 5 ;i++  {
            //读取管道中的数据
            num := <-buff
            fmt.Println("------消费者消费", num)
        }
        //利用管道阻塞解决死循环问题
        exitChan<- 666
    }
    func main() {
        //定义两个双向管道
        myChan := make(chan int, 5)
        exitChan := make(chan int)
    
        //开启两个go程
        go producter(myChan)
        go consumer(myChan,exitChan)
    
        <-exitChan
    }
    

    select选择结构
    • 在企业开发中, 一般情况下使用select都是用于同时消费多个管道中数据
    • 在企业开发中, 一般情况下select中的default不用写
    • 在企业开发中, 一般情况下使用select来控制退出主线程
    • 在企业开发中, 一般情况下使用select来处理超时
    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        // 1.创建一个管道
        myCh1 := make(chan int, 5)
        //myCh2 := make(chan int, 5)
        //exitCh := make(chan bool)
    
        // 2.开启一个协程生产数据
        go func() {
            time.Sleep(time.Second * 5)
            for i := 0; i < 10 ; i++ {
                myCh1<-i
                fmt.Println("生产者1生产了", i)
            }
            close(myCh1)
            //exitCh<-true
        }()
        /*
       go func() {
           time.Sleep(time.Second * 5)
           for i := 0; i < 10 ; i++ {
               myCh2<-i
               fmt.Println("生产者2生产了", i)
           }
           close(myCh2)
       }()
    
        // 2.在主线程中消费数据
       //for i := 0; i < 10 ; i++ {
       //   num := <-myCh
       //   fmt.Println("------消费者消费了", num)
       //}
    
       //for num := range myCh{
       //   fmt.Println("------消费者消费了", num)
       //}
        */
        // 注意点: 在企业开发中, 一般情况下使用select都是用于同时消费多个管道中数据
        //         在企业开发中, 一般情况下select中的default不用写
        //         在企业开发中, 一般情况下使用select来控制退出主线程
        //         在企业开发中, 一般情况下使用select来处理超时
        for{
            //fmt.Println("start")
            select {
            case num1 := <-myCh1:
                fmt.Println("------消费者消费了myCh1", num1)
                //case num2 := <-myCh2:
                //  fmt.Println("------消费者消费了myCh2", num2)
                //case <-exitCh:
                //  return
            case <-time.After(3):
                fmt.Println("超时了")
                return
                //default:
                //  fmt.Println("生产者还没有生产好数据")
            }
            //fmt.Println("=====================")
            time.Sleep(time.Millisecond)
        }
        fmt.Println("程序结束了")
    }
    

    管道是地址传递
    package main
    
    import "fmt"
    
    func main() {
        //定义一个双向有缓冲区管道
        var myChan = make(chan int,3)
        fmt.Println(myChan) //0xc00007c080
        fmt.Printf("%p\n", myChan) //0xc00007c080
        fmt.Printf("%p\n", &myChan)  //0xc000072018
    
        //管道是地址传递
        //定义一个单向管道
        var myChan2 chan <- int
        myChan2 = myChan
        //打印单向管道len和cap
        fmt.Println("len = ", len(myChan2),"cap = ", cap(myChan2))
        //打印双向管道len和cap
        fmt.Println("len = ", len(myChan),"cap = ", cap(myChan))
    }
    

    定时器
    • 对时间的操作方法,一般都在time包中查找
    package main
    
    import (
        "fmt"
        "time"
    )
    
    func main() {
        /*
        type Timer struct {
            C <-chan Time
            r runtimeTimer
        }
        */
    
        //1.使用定时器,就要用到time包
        // NewTimer作用, 就是让系统在指定时间之后, 往Timer结构体的C属性中写入当前的时间
        // 让程序阻塞3秒, 3秒之后再执行
        //func NewTimer(d Duration) *Timer
    
        /*start := time.Now()
        fmt.Println(start)
        //使用定时器
        timer := time.NewTimer(time.Second * 3)
        fmt.Println(<-timer.C)*/
    
        //2.func After(d Duration) <-chan Time
        //这个定时器底层就是NewTimer实现的
        /*start := time.Now()
        fmt.Println(start)
        //使用定时器
        timer := time.After(time.Second * 3)
        fmt.Println(<-timer)*/
    
        // 以上的定时器都是一次性的定时器, 也就是只会执行一次
        /*go func() {
            start := time.Now()
            fmt.Println(start)
            timer := time.After(time.Second * 3)
            for  {
                fmt.Println(<-timer)
            }
        }()
    
        for  {
            ;
        }*/
    
        //周期性定时器
        start := time.Now()
        fmt.Println(start)
        //定义周期性定时器
        timer := time.NewTicker(time.Second * 2)
        for {
            fmt.Println(<-timer.C)
            timer.Stop()
        }
    }
    

    相关文章

      网友评论

        本文标题:22-Go语言管道(Channel)

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