Go 快速开发 | 11 - Go 并发编程

186 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

一、并发编程

并发与并行

  • 并发:多线程在一个核的 CPU 上运行
  • 并行:多线程在多个核的 CPU 上运行

并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。这就好像两个人用同一把铁锨,轮流挖坑,两个人非常快速的使用同一把铁锹交替挖坑,快到就好像一直在挖。

当有多个线程在操作时,如果系统只有一个CPU,根本不可能真正的同时进行一个以上的线程,它只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent)。

并发的本质还是串行。

image.png

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。就好像两个人各拿一把铁锨在挖坑,一小时后,每人一个大坑。所以无论从微观还是从宏观来看,二者都是一起执行的。

image.png

进程、线程以及协程

进程:一段程序的执行过程。

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
  • 程序是指令、数据及其组织形式的描述,而进程是程序的实体。
  • 进程是线程的容器。

线程:轻量级进程,是 CPU 的最先调度单位

  • 线程是程序的实际执行者,一个进程至少包含一个主线程以及多个子线程
  • 多个线程共享资源
  • 线程间通信主要高共享内存,上下文切换较快,资源开销少,相比进程不易丢失数据

协程:又称为轻量级线程

  • 一个线程包含多个协程,协程有独立的占空间,多个协程贡献堆空间
  • 开启一个协程需要 2k 空间,远低于开启一个线程需要的 2M 空间,且可以自己调度
  • 协程是用户级线程,由用户自己调度,而线程需要通过 CPU 调度

二、goroutine

在使用 Java 进行编发编程时,通常需要自己维护一个线程池,并且需要自己包装任务以及调度线程执行任务并维护上下文切换。

goroutine 是 Go 语言中协程的实现(轻量级线程),由 Go 运行时(runtime) 调度和管理,Go 程序会将 goroutine 中的任务合理的分配给每个 CPU,这是应为 Go 在语言层面内置了调度和上下文切换的机制。

Go 程序从 main 包中的 main() 函数开始,在程序启动时就会为 main() 函数创建一个默认的 Go 协程(goroutine)

Go 语言编程不需要再自己写进程、线程,当需要某个任务并发执行时,只需要将这个任务包装成一个函数

//noinspection ALL
func main(){
   // 开启一个协程
   go testCoroutine()

   for i := 0; i < 2; i++ {
      fmt.Println("main() 函数执行")
      time.Sleep(time.Microsecond * 100)
   }

}

func testCoroutine(){
   for i := 0; i < 10; i++ {
      fmt.Println("testCoroutine() 函数执行")
      time.Sleep(time.Microsecond * 100)
   }
}

执行上述代码,输出结果如下:

main() 函数执行
testCoroutine() 函数执行
main() 函数执行
testCoroutine() 函数执行

上述代码中主线程中每 100ms 打印一次,总共打印 2 次,通过 go 另起一个协程,打印 10 次 打印是交替的,说明是并行的;另起的协程只打印了 2 次,因为主线程循环两次就退出了。

WaitGroup

通过上述的例子可以确定,当主线程结束后,主线程中包含的协程无论有没有执行完成都会随主线程一起结束,如果需要主线程等待协程执行完毕,那么就需要用到 WaitGroup。

WaitGroup 内部维护着一个计数器,通过计数器来判断所有并发任务是否完成,当启动了 N 个并发任务时,通过调用 Add() 方法就将计数器增加 N,每执行完一个任务都通过调用 Done() 方法使计数器 -1,通过调用 Wait() 方法等待并发任务执行完,当计数器为 0 时,所有并发任务完成。

// 定义一个全局计数器
var waitGroup sync.WaitGroup

//noinspection ALL
func main(){
   waitGroup.Add(1)
   // 开启第一个任务
   go testCoroutineWithWaitGroup1()
   
   // 开启第二个任务
   waitGroup.Add(1)
   go testCoroutineWithWaitGroup2()

   waitGroup.Wait()

   for i := 0; i < 2; i++ {
      fmt.Println("main() 函数执行")
      time.Sleep(time.Microsecond * 100)
   }

}

func testCoroutineWithWaitGroup1(){
   for i := 0; i < 5; i++ {
      fmt.Println("testCoroutineWithWaitGroup1() 函数第", (i+1),"次执行")
      time.Sleep(time.Microsecond * 100)
   }

   // 函数执行完,计数器 -1
   waitGroup.Done()
}

func testCoroutineWithWaitGroup2(){
   for i := 0; i < 5; i++ {
      fmt.Println("testCoroutineWithWaitGroup2() 函数第", (i+1),"次执行")
      time.Sleep(time.Microsecond * 100)
   }
   // 函数执行完,计数器 -1
   waitGroup.Done()
}

执行上述代码,输出结果如下:

testCoroutineWithWaitGroup2() 函数第 1 次执行
testCoroutineWithWaitGroup1() 函数第 1 次执行
testCoroutineWithWaitGroup2() 函数第 2 次执行
testCoroutineWithWaitGroup1() 函数第 2 次执行
testCoroutineWithWaitGroup2() 函数第 3 次执行
testCoroutineWithWaitGroup1() 函数第 3 次执行
testCoroutineWithWaitGroup2() 函数第 4 次执行
testCoroutineWithWaitGroup1() 函数第 4 次执行
testCoroutineWithWaitGroup2() 函数第 5 次执行
testCoroutineWithWaitGroup1() 函数第 5 次执行
main() 函数执行
main() 函数执行

相比上一次,这次主线程是在两个协程任务执行完成之后才执行并退出的。

使用 WaitGroup 来让主线程等待协程任务执行需要四个步骤:

  1. 首先定义一个全局计数器
  2. 在每个函数/任务的代码块末尾(既函数执行结束时)调用函数将计数器 -1
  3. 在 main 函数中执行函数前调用计数器 +1
  4. 在主线程结束执行调用等待方法,直到计数器为 0,所有任务执行完成后,才会调用主线程中等待方法后面的代码