Go的协程| 青训营笔记

47 阅读4分钟

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

协程、线程与进程

进程、线程和协程之间概念的区别,:

对于 进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行抢占式调度(有多种调度算法)。而对于协程(用户级线程),这是对内核透明的,也就是系统并不知道有协程的存在,是完全由用户自己的程序进行调度的,因为是由用户程序自己控制,那么就很难像抢占式调度那样做到强制的 CPU 控制权切换到其他进程/线程,通常只能进行 协作式调度,需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。

​ 由于Golang在runtime、系统调用等多方面对goroutine调度进行了封装和处理,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU转让出去,让其他goroutine能被调度并执行,也就是说Golang从语言层面支持了协程。Golang的一大特色就是从语言层面原生支持协程,在函数或者方法前面加 go关键字就可创建一个协程。

​ 协程的优势:协程在切换开销方面,goroutine远比线程小消耗小。且协程默认占用内存远比 Java 、C 的线程少。线程通常大小是MB级别,而协程通常大小为KB级别。

协程的定义和使用

通过在方法前加go关键字的形式可以立刻开启一个协程,注意我们的main函数本身也是一个协程。如果不加任何限制,main线程不会等待其他子协程就结束执行。例如下面的程序中控制台就不会有输出:

package main
import "fmt"

func add(x, y int)  {
   z := x + y
   fmt.Println(z)
}

func main() {
   for i:=0; i<10; i++ {
      go add(i, i)
   }
}

利用sync.WaitGroup来实现协程的同步

sync.WaitGroup开箱即用,其是并发安全的。原理类似于Java中的CountDownLatch,可以想象该类型中有一个计数器,默认值是0。WaitGroup拥有三个指针方法,下面的方法就是操作或判断计数器:

Add : 增加或减少计数器的值。一般情况下,会用这个方法来记录需要等待的goroutine的数量 Done : 用于对其所属值中计数器的值进行减一操作,就是Add(-1),可以在defer语句中调用它 Wait : 阻塞当前的goroutine,直到所属值中的计数器归零。

如下示例,将wg 计数设置为10, 每个for循环运行完毕都把计数器减一,主函数中使用Wait() 一直阻塞,直到wg为零(也就是所有的10个for循环都运行完毕)时,主程序main函数才会执行结束。

func main() {
   waitGroup := sync.WaitGroup{}
   waitGroup.Add(10)
   for i := 0; i <= 9; i++ {
      go func(i int, j int) {
         defer waitGroup.Done()
         fmt.Println("执行加法:", i)
         z := i + j
         fmt.Println("结果为:", z)
         //计数器减1
      }(i, 1)
   }
   //主协程等待waitGroup计数器归零
   waitGroup.Wait()
}

协程之间的通信

协程之间的通信可以通过共享内存实现通信,也可以通过通信来实现共享内存。 go支持上述两种方式,但是更提倡后者,即通过通信来实现共享内存。

通过通道来实现通信

通道类似于一个队列,里面拥有一个可变长度的缓冲区,遵循先入先出的原则。 创建通道的方式: 方式1:

var  通道名   chan 类型

方式2:通过make函数,其中缓冲区大小可以省去,则通道中无缓冲通道,称为同步通道。通道缓冲区代表了可存储的空间大小。

ch:=make(chan 元素类型,[缓冲大小])

channel <- 变量名 表示将对应的变量存入通道缓冲区。 变量名:= <-channel 表示从通道中取出一个变量。

由go实现一个经典的消费生产模型非常简单:

func main() {
   channel := make(chan int, 5)
   go producer(channel)
   go consumer(channel)
   time.Sleep(time.Second)

}
func consumer(channel chan int) {
   if channel != nil {
      for {
         res := <-channel
         fmt.Println("取出", res)
      }
   }
}
func producer(channel chan int) {
   if channel != nil {
      for {
         random := rand.Int()
         channel <- random
         fmt.Println("存入", random)
      }
   }
}

当协程无缓冲区时,在向某通道发送消息之后,会阻塞当前线程,直到消息被取出消费。当协程有缓冲区时,协程会等到通道缓冲区填满时才阻塞。

在向管道中发送数据时,发送的数据和原数据是独立的,因此修改原数据不会影响发送的数据。

通过互斥锁实现共享内存

Golang也支持互斥锁。

命名: lock := sync.Mutex

加锁: lock.lock()

解锁: lock.unlock()

sync.Mutex性能开销较大,因此应该用于保护一段逻辑,而不是保护一个变量。