Go语言并发、锁、channel

  • 多线程同时执行叫做并行
  • 并发就是在不同线程中来回切换执行来达到并行的效果就是并发
  • 通过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)
    }
    原文作者:AuglyXu
    原文地址: https://www.jianshu.com/p/163a721dfdd8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞