GO进阶之协程开发 | 青训营笔记

84 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天

GO进阶之协程开发

一、并发编程

1.1Goroutine概念

  • 「线程」内核态,线程跑多个协程,栈 MB 级别
  • 「协程」用户态,轻量级线程,栈 KB 级别

【使用】在函数前面加个go关键字,即可开启goroutine,但是要注意:主线程若执行结束,则这些协程都将关闭。

1.2 协程间通信

总共分为两种

  • 通过通信共享内存
  • 通过共享内存实现通信

在Go中,提倡通过通信共享内存而不是通过共享内存而实现通信,用平常话说就是尽量使用通道进行单向的传输,而不是双向。

channel定义

切记:channel 的声明必须使用 make 关键字,不能直接 var c chan int,这样得到的是 nil channel

channel 声明方法

  • 双向通道:格式:make(chan 元素类型, [缓冲大小])缓冲大小是可选参数,其代表着有无缓冲通道

    • 无缓冲通道(同步通道)----make(chan int)
    • 有缓冲通道---make(chan, int, 2)
  • [常用于函数]只读通道:只能从里读(chan是箭头的起点),例如:func foo(ch1 <-chan int) // 只能从 ch1 里读

  • [常用于函数]只写通道:只能往里写(chan是箭头的终点),例如:func bar(ch2 chan<- int) // 只能往 ch2 里写

channel 使用方法

这里要注意:变量v 需要和 ch 声明的元素类型相同

  • channel 发送值:ch <- v
  • channel 里读取结果:v <- ch

    • 注意通道的关闭函数close(),会对 ch 发送一条消息,这个动作可以用来通知一些设定的 goroutine 退出。
    • 并且读通道有个和map特定的一个方法:v, f = <-ch //当关闭ch时,f返回 false
    • package main
      ​
      func main() {
          done := make(chan struct{})  //这个done用于阻塞主线程,等待协程结束
          c := make(chan int)  //主线程生产传给协程
      ​
          go func() {
              //这里调用close,会给done发消息,因此主线程的<-done就会解除阻塞
              //主线程就能次于协程结束了!
              defer close(done)
              for {
                  x, ok := <-c //close(c)时会收到一条消息,x值为0,ok为false
                  if !ok { 
                      return //退出协程
                  }
                  println(x)
              }
          }()
          c <- 1
          c <- 2
          c <- 3
          close(c)
          <-done // close 时会收到消息,解除阻塞
      }
      

【如何循环取数】上面的代码里,我们用到了一个无限循环的方法,但正常使用,尝试用for-range的方法,可用下面的方法来代替:

//使用 for-range 遍历 channel 会比使用 ok-idiom(就是x:ok) 更简洁
//c被close时,会退出for-range循环
for x := range c {
    println(x)
}
/*
for {
          x, ok := <-c //close(c)时会收到一条消息,x值为0,ok为false
          if !ok { 
              return //退出协程
          }
          println(x)
}
*/

channel 关闭的基本法则

  • sender 的情况下,都可以直接在 sender 端关闭 channel
  • sender 的情况下,可以增加一个传递关闭信号的 channel 专门用于关闭数据传输的 channel

原则:不要从接收端关闭 channel,也不要在有多个发送端时,主动关闭 channel

channel 对于有无缓冲通道的阻塞情况

select语句

select 是 Go 中的一个控制结构,类似于 switch 语句。

select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。

【规则】select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行channel 的声明必须使用 make 关键字,不能直接 var c chan int,这样得到的是 nil channel相应的代码块。

  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。

    否则:

    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码
​
    // 你可以定义任意数量的 case
​
  default:
    // 所有通道都没有准备好,执行的代码
}

【用法】

  1. 阻塞main函数。有时候我们会让main函数阻塞不退出,如http服务,我们会使用空的select{}来阻塞main,比空for更省性能

  2. 竞争选举。这个是最常见的使用场景,多个通道,有一个满足条件可以读取,就可以“竞选成功”

  3. 超时处理,如:

    select {
        case str := <- ch1
            fmt.Println("receive str", str)
        case <- time.After(time.Second * 5): 
            fmt.Println("timeout!!")
    }
    
  4. 判断buffered channel是否阻塞

这个例子很经典,比如我们有一个有限的资源(这里用buffer channel实现),我们每一秒向bufChan传送数据,由于生产者的生产速度大于消费者的消费速度,故会触发default语句,这个就很像我们web端来显示并发过高的提示了

package main
import (
    "fmt" 
    "time"
)
​
func main()  {
    bufChan := make(chan int, 5)
    
    go func ()  {
        time.Sleep(time.Second)
        for {
            <-bufChan//消费者
            time.Sleep(5*time.Second)
        }
    }() 
    
​
    for {
        select {    
        case bufChan <- 1:  //生产者
            fmt.Println("add success")
            time.Sleep(time.Second)  
        default:        
            fmt.Println("资源已满,请稍后再试")
            time.Sleep(time.Second) 
        } 
    }
}

defer语句

defer是Go语言中的延迟执行语句,用来添加函数结束时执行的代码,常用于释放某些已分配的资源、关闭数据库连接、断开socket连接、解锁一个加锁的资源。Go语言机制担保一定会执行defer语句中的代码。

类似于C++的析构~

当有多个defer语句时,顺序是后入先出,如下例:

func main{
    if true {
        defer fmt.Printf("1")
    } else {
        defer fmt.Printf("2")
    }
    defer fmt.Printf("3")
    //答案是3 1
}