浅谈一下Go的并发编程 | 青训营笔记

158 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。

可以关注一下我的博客 wuhlan3.gitee.io/ 呀!

一、概述

Go 是并发式语言,而不是并行式语言。

并发和并行是老生常谈的问题了,这里就简单抛出一下定义:

  • 并行是指两个或者多个事件在同一时刻发生(是一种通过多处理器来提高效率的能力);
  • 并发是指两个或多个事件在同一时间间隔发生(可在单处理上,也可在多处理器上)。

Go语言原生支持并发。Go 使用Go 协程(Goroutine) 和信道(Channel)来处理并发。Go没有把并行放在首要位置,因为并发程序可能是并行的,也可能不是。一个设计良好的并行程序在并行方面也会表现非常出色。

二、Go协程

1.Go 协程是什么?

Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。

2.Go协程与线程的对比

特点线程协程
拥有的资源程序计数器、寄存器、栈和状态字寄存器上下文和栈
内存消耗MB级别KB级别
线程/协程切换需要从用户态切换到内核态,调用内核提供的syscall底层函数。(保存和设置程序计数器、少量寄存器和栈)用户态进行简单的现场保护和恢复即可(保存和设置寄存器上下文和栈)。

3.启动一个Go协程

func Hello() {
   fmt.Println("Hello")
}

func main() {
   go Hello()
   fmt.Println("world")
}

多运行几遍,我们会发现有的时候可以输出两句话,有的时候只能输出一句话。为什么呢?这是因为启动了一个协程之后,主协程不会等待子协程,而是继续往下执行。如果主协程终止了,整个程序就终止了,所以子协程就没办法输出了。所以我们可以稍微改进一下。

func Hello() {
   fmt.Println("Hello")
}

func main() {
   go Hello()
   time.Sleep(time.Second)
   fmt.Println("World")
}

但是问题还是没有彻底解决。这里又引出了新的问题:我们怎么知道需要等待多久呢?等待时间短了不能确保子协程完整运行,等待时间长了,影响协程效率。这里就引出了同步的概念,Go编程语言中可以使用WaitGroup来进行同步。

三、同步与互斥

1.WaitGroup

WaitGroup是一个结构体,它包含着一个计数器且初始化为0。基本的方法如下:

  • Add(n int) 可以是计数器加上n,通常在启动n个go协程之前调用;
  • Done()可以使计数器减一,通常在一个go协程运行结束后调用;
  • Wait()方法会阻塞调用它的 Go 协程,直到计数器变为 0 后才会停止阻塞。

所以我们可以对之前的程序进行改写。只有当3个子协程都运行结束了,主协程才会继续执行。

func Hello(wg *sync.WaitGroup) {
   defer wg.Done()
   fmt.Println("Hello")
}

func main() {
   var wg sync.WaitGroup
   for i := 0; i < 3; i++ {
      wg.Add(1)
      go Hello(&wg)
   }
   wg.Wait()
   fmt.Println("World")
}

2.无缓冲信道

除了WaitGroup之外,我们还可以使用Channel来进行同步。首先来了解一下channel的基本用法:

  • 初始化 a `` := make(chan ``int``),表示定义了一个int类型的channel;
  • a <- data 写入信道a
  • data := <- a 读取信道 a

可以看到,信道的使用方法是非常形象的,它就像一个管道一样,箭头标明了数据的流动方向。

那么我们该如何改写上面的程序呢?具体实现如下:

3.缓冲信道

无缓冲信道在发送和接收的过程中都是阻塞的。就是说,当信道里有数据的时候,协程需要一直等待,直到没有数据才能够写信道;当信道里没有数据的时候,协程也需要一直等待,直到有数据了,才能读取数据。

我们也可以创建一种有缓冲的信道,只有缓冲满了,才会阻塞协程继续写入数据:

a := make(chan int, 3),创建了一个容量为3的信道。

可以参考训练营里一个简单的生产者消费者例子:

  • 这里一个协程负责生产数字0~9;
  • 一个协程负责计算平方,并发送给主协程
func CalSquare() {
   src := make(chan int)
   dest := make(chan int, 3) //考虑消费者的速度会稍微慢一些,所以使用带缓冲的队列,
                             // 避免消费速度慢而导致生产速度慢
   //这里使用了匿名函数

   go func() {
      defer close(src)
      for i := 0; i < 10; i++ {
         src <- i
      }
   }()

   go func() {
      defer close(dest)
      for i := range src {
         dest <- i * i
      }
   }()
   for i := range dest {
      println(i)
   }
}

4.互斥锁

Go中的互斥锁使用方式比较简单,就不再赘述了。

  • 定义全局的变量lock sync.Mutex
  • 加锁lock.Lock()
  • 释放锁lock.Unlock()

... 以上是青训营提到的比较基础的并行编程内容。后续还要探究select、工作池等内容。

参考资料

[1] 字节跳动青训营——Go语言上手 - 工程实践

[2] studygolang.com/