美文网首页
Go语言并发、锁、channel

Go语言并发、锁、channel

作者: AuglyXu | 来源:发表于2018-10-11 15:04 被阅读0次
    • 多线程同时执行叫做并行
    • 并发就是在不同线程中来回切换执行来达到并行的效果就是并发
    • 通过go可以在当前线程中开启一个协程
    • 保证协程被执行,那么主线程不能挂掉

    runtime包中常用的方法

    • runtime.Gosched()
    • 作用:用于出让本次的执行权限
    • 注意点:让出本次执行权限并不意味着不再执行
    • 应用场景: 让某些协程执行的次数少一次, 让某些协程执行的次数多一些

    • runtime.Goexit()
    • 作用:终止调用它的go程,其他go程不受影响
    • 注意点:这个go程将不会再被调用

    • runtime.NumCPU()

    • 获取本地机器的逻辑CPU个数

    • runtime.GOMAXPROCS(num)

    • 设置可同时执行的最大CPU数,并返回上一次设置的CPU个数

    • 注意点:Go1.8之后, 系统会自动设置为最大值,了解------>忘记


    互斥锁

    • 当使用互斥锁后,当前go程将不被再次调用
    • 案例一:有序打印
        /*
        需求: 定义个打印的函数, 可以逐个的打印传入字符串的每个字符
        在两个协程中调用这个好方法, 并且还要保持输出的内容有序的
         */
      var lock = sync.Mutex{}
      func printer(str string){
      lock.Lock()//添加锁,如果不添加那么可能执行输出hello也可能执行输出world,那么就是无序的
        for _,val := range str{
            fmt.Printf("%c", ch)
            time.Sleep(time.Millisecond * 300)
        }
      lock.Unlock()
      }
    func main(){
      go printer("hello")
      go printer("world")
    for{
        ;
      }
    }
    

    互斥锁的资源抢夺问题

    • 注意点:如果两个go程上了同一把锁,那么当一个go程被锁上时,另一个go程也会被锁住
    • 案例二:生产者和消费者
      var lock = sync.Mutex{}
      var buff = [10]int
      func producer(){
        lock.Lock()
        rand.Seed(time.Now().UnixNano())
        for i:=0; i < 10 ;i++{
            num := rand.Intn(100)
            fmt.Println("生产者生产了",num)
            buff[i] = num
            time.Sleep(time.Millisecond * 300)
        }
        lock.Unlock()
      }
    
     func consumer(){
        lock.Lock()
        for i:=0; i < 10 ;i++{
            buff[i] = num
            fmt.Println("消费者消费到了",num)
        }
        lock.Unlock()
      }
    
    func main() {
    
        go producer()
        // 我们想要的是, 只有生产者生产了, 我们才能消费
        // 注意点: 在多go程中, 如果生产者生产的太慢, 那么消费者就会消费到错误的数据
        go consumer()
        // 注意点: 看上去通过给生产者以及消费者同时加锁就能解决, 只有生产完了才能消费
        //         但是取决于谁想执行加锁操作, 所以不完美
        for{
            ;
        }
    }
    

    • 在上述案例中,只能一个生产者对应一个消费者,当有第二个生产者或者第二个消费者时会因为并发而产生数据混乱。

    • 在上述案例中,无法判断先执行消费者还是先执行生产者,如果先进入了调用者的go程,则会取不到数据,就会发生数据混乱

    • 为了解决上述问题,我们可以用管道来解决这个问题

    管道

    • 管道是一种数据类型,和字典切片很像,不用make函数创建就使用会报错
    • 格式: var 变量名称 chan 数据类型 ---------> var myCh chan int
    • 作用:在Go语言的协程中, 一般都使用管道来保证多个协程的同步, 或者多个协程之间的通讯
    var myCh chan int
    myCh = make(chan int, 3)
    
    • 以上代码创建了一个容量为3的管道(注意:长度默认为0,添加数据以后长度会动态变化)

    管道的使用

    管道写入数据

    • myChan<-
    var myCh chan int
    myCh = make(chan int, 3)
    myCh<-666  //写入了一个数据666
    

    管道读取数据

    • <-myChan
    var myCh chan int
    myCh = make(chan int, 3)
    myCh<-666  //写入了一个数据666
    fmt.Println(<-myCh) //读取了666
    

    管道写入和读取的注意点

    • 没有创建管道容量直接使用会报错
    var myCh chan int
    myCh<-666  //报错
    
    • 管道中没有数据时读取会报错
    var myCh chan int
    myCh = make(chan int, 3)
    fmt.Println(<-myCh) //报错
    
    • 管道已满,再向管道中写入数据会报错
    var myCh chan int
    myCh = make(chan int, 3)
    myCh<-1 //写入了一个数据1
    myCh<-2 //写入了一个数据2
    myCh<-3 //写入了一个数据3
    myCh<-4 //报错
    

    管道的关闭

    • close(管道名称)
    • 注意点:管道关闭以后不能再管道中写入数据,但是可以在管道中读取数据

    管道的遍历

    • 可以使用for循环, 也可以使用 for range循环, 以及死循环来遍历。
    • 更推荐使用后两者。因为在企业开发中, 有可能我们不知道管道中具体有多少条数据, 所以如果利用for循环来遍历, 那么无法确定遍历的次数, 并且如果遍历的次数太多, 还会报错

    for range遍历

    • 注意点:在写完数据以后必须关闭管道,否则会报错
    • 一般企业开发中,管道数据写完之后都会将管道关闭
    var myCh chan int
    myCh = make(chan int, 3)
    myCh<-1 //写入了一个数据1
    myCh<-2 //写入了一个数据2
    myCh<-3 //写入了一个数据3
    close()//管道必须关闭,否则报错
    for v := range myChan{
      fmt.Println(v) //先后输出 1 2 3
    }
    

    死循环遍历

    • 注意点: 如果被遍历的管道没有关闭, 那么会报错
    • 如果管道被关闭, 那么会将true返回给ok, 否则会将false返回给Ok
    var myCh chan int
    myCh = make(chan int, 3)
    myCh<-1 //写入了一个数据1
    myCh<-2 //写入了一个数据2
    myCh<-3 //写入了一个数据3
    close()//管道必须关闭,否则报错
    for{
      if value,ok:= <-myChan;ok{
           fmt.Println(v) //先后输出 1 2 3
      }
    }
    

    管道的阻塞现象(重点)

    • 单独在主线程中操作管道, 写满了会报错, 没有数据去读取也会报错

    • 只要在go程中操作管道, 无论有没有写满, 无论有没有数据都会发生管道阻塞的现象

    • 阻塞现象(和输入缓冲区很相似)

      • 在go程中,如果写满了,再写入数据,则不会报错,等待管道中的数据被取出后再添加

      • 在go程中,没有数据还在被取出,则不会报错,等待管道中的数据被写入后再取出

    利用管道阻塞实现并发串行

      var myChan chan int
      myChan = make(chan int,10)
      func producer(){
        rand.Seed(time.Now().UnixNano())
        for i:=0; i < 10 ;i++{
            num := rand.Intn(100)
            myChan <- num
            fmt.Println("生产者生产了",num)
            time.Sleep(time.Millisecond * 300)
        }
      }
    func producer2(){
        rand.Seed(time.Now().UnixNano())
        for i:=0; i < 10 ;i++{
            num := rand.Intn(100)
            myChan <- num
            fmt.Println("生产者生产了",num)
            time.Sleep(time.Millisecond * 300)
        }
      }
    
     func consumer(){
        for i:=0; i < 10 ;i++{
            num := <-myChan
            fmt.Println("消费者消费到了",num)
        }
      }
    
    func main() {
        go producer()
        go producer2()
        go consumer()
        for{
            ;
        }
    }
    
    • 以上代码可以不止一个生产者或者消费者

    利用管道阻塞解决最后写死循环保证主线程不挂

    • 注意点: 如果这个管道在协程中有被使用,那么编译器会认为主线程在取数据的时候会等待协程中输入,并不会报错
    • go程执行完后才向管道中填充数据
        var myCh = make(chan int, 3)
        var exitCh = make(chan bool, 1)
        go func() {
            for i:=0; i<3; i++ {
                fmt.Println("生产了数据", i)
                myCh<-i
            }
            exitCh<-true
        }()
        fmt.Println("exitCh之前的代码")
        <-exitCh // 阻塞
        fmt.Println("exitCh之后的代码")
    

    无缓冲管道

    • 无缓冲管道没有容量,在主程中既不可以读也不可以写
    • 无缓冲区管道只能存在在go程中,并且如果在同一个go程中,无论先读或者先写都会阻塞
    • 读写必须都存在,但如果都在主程中会报错,在同一个go程中会阻塞
    • 无缓冲管道如果存在在不同的go程中,先读或者先写无所谓
       //在go程中可以只读或者只写,会阻塞
        myCh := make(chan int, 0)
        go func() {
            fmt.Println("123")
            myCh<-998
            //<-myCh
            fmt.Println("abc")
        }()
    

    无缓冲管道解决死循环

    //定义一个没有缓冲的管道
        exitCh := make(chan bool)
    
        go func() {
            for i:= 0; i < 5; i++ {
                myCh<-i
                fmt.Println("生产了", i)
            }
            exitCh<-true
        }()
        //for{
        //  ;
        //}
        //time.Sleep(time.Second)
        <-exitCh
    

    单向管道和双向管道

    • 默认情况下所有的管道都是双向的管道(可读可写)

    • 那么在企业开发中, 我们可能会需要将管道作为函数的参数, 并且还需要限制函数中如何使用管道,那么这个时候我们就可能会使用单向管道

    • 格式:
      双向格式:
      var myCh chan int;
      myCh = make(chan int, 5)
      myCh = make(chan int)

      单向格式:
      var myCh chan<- int; 只写的管道
      var myCh <-chan int; 只读的管道

    • 注意点:
      1.双向管道可以赋值给单向管道
      2.单向管道赋值给双向管道会报错

    单向管道作为函数参数
    • 增强了代码的语义
    • 管道是地址传递
    // 定义一个函数模拟生产者
    func producer(buff chan<- int)  {
        rand.Seed(time.Now().UnixNano()) // 种随机种子
        for i:=0; i<5;i++  {
            // 产生随机数
            num := rand.Intn(100)
            fmt.Println("生产者生产了", num)
            // 将生产好的数据放入缓冲区
            buff<-num
            //time.Sleep(time.Millisecond * 300)
        }
    }
    
    // 定义一个函数模拟消费者
    func consumer(buff <-chan int, exitCh chan<- int)  {
        for i:=0; i<5;i++  {
            num := <-buff
            fmt.Println("-------消费者消费到了", num)
        }
        exitCh<-666
    }
    
    func main() {
        // 定义一个数组模拟缓冲区
        var buff = make(chan int, 5)
        var exitCh = make(chan int)
        go producer(buff)
        go consumer(buff, exitCh)
    
        <-exitCh
        fmt.Println("程序结束了")
        //for{
        //  ;
        //}
    }
    

    管道是指针类型

        var myCh1 chan int = make(chan int, 5)
        //fmt.Println(myCh1) // 0xc042094000
        //fmt.Printf("%p\n", myCh1) // 0xc042094000
        //fmt.Printf("%p\n", &myCh1) // 0xc042082018
        myCh1<-1
        myCh1<-2
    
        var myCh2 <-chan int
        myCh2 = myCh1 // 将双向的管道转换成单向的管道
        // 打印单向管道的长度和容量
        fmt.Println("len", len(myCh2), "cap", cap(myCh2))//len 2 cap 5
        fmt.Println(<-myCh2)//1
        // 打印双向管道的长度和容量
        fmt.Println("len", len(myCh1), "cap", cap(myCh1))//len 1 cap 5
    

    select结构

    • select选择结构和switch很像,如果所有case不满足则会执行default
    • 企业开发中一般不会使用default,因为容易经常跑进default
    • 企业开发中,一般通过select用于消费多个管道中的数据
    • 企业开发中,一般通过select控制是否超时
    • 企业开发中,一般通过select控制退出主线程
    • 以下是一个生产消费的案例
        // 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)
        }()
    
    for{
            select {
            case num1 := <-myCh1:
                fmt.Println("------消费者消费了myCh1", num1)
            case <-time.After(3):
                fmt.Println("超时了")
                return
            }
            time.Sleep(time.Millisecond)
        }
        fmt.Println("程序结束了")
    

    定时器

    • 想使用定时器需要使用time包

    一次性定时器

    NewTimer函数

    • 有一个Time结构体
      • type Timer struct {
        C <-chan Time
        r runtimeTimer
        }
    • 作用, 就是让系统在指定时间之后, 往Timer结构体的C属性中写入当前的时间
    • NewTimer的函数接收一个时间,代表阻塞多少秒后写入时间
    • 注意:该函数返回的是一个Time结构体,要调用其属性必须 名称.属性
         start := time.Now()
         fmt.Println(start) //打印当前时间
         timer := time.NewTimer(time.Second * 3) // Timer
         fmt.Println(<-timer.C) //打印三秒后的时间
    

    After函数

    • After的函数接收一个时间,代表阻塞多少秒后写入时间
    • 注意:该函数返回的是一个Time结构体中C的属性
        start := time.Now()
        fmt.Println(start)
        timer := time.After(time.Second * 3) // Timer.C
        fmt.Println(<-timer)
    

    周期性定时器

    • time包中有一个NewTicker的函数,接收一个时间,表明阻塞多少秒向Time结构体中写入数据
    • 注意点:该函数会反复往结构体中写入数据,所以需要关闭,可以用stop函数进行关闭
        start := time.Now()
        fmt.Println(start)
        ticker := time.NewTicker(time.Second * 2)
        for{
            fmt.Println(<-ticker.C)
        }
    

    相关文章

      网友评论

          本文标题:Go语言并发、锁、channel

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