1.4.2 Go高并发编程
虽然Go语言已经足够简单了,但对于很多Go初学者来说,Go语言中的并发问题依然让人防不胜防。如何能够编写出高性能、稳定的Go程序呢?这就需要开发者对Go高并发编程有一定了解。图1-9列举了Go高并发编程涉及的核心知识点。
图1-9 Go高并发编程
参考图1-9,接下来将按照顺时针方向逐个介绍Go高并发编程需要掌握的每一项知识点。
(1)GMP调度模型
相信每一个Go开发者都或多或少听过GMP调度模型。G(goroutine)代表协程;M(machine)代表线程,运行调度程序,负责协程G的调度执行;P(processor)代表逻辑处理器,可以理解为一种资源,线程M必须绑定逻辑处理器P才能调度协程G。
协程被称为轻量级、用户态线程。协程可以像线程一样并发执行,只是你有想过为什么吗?想要完全理解协程,先要对汇编程序、寄存器、Linux虚拟内存结构等有一定的了解,因为Go语言协程的切换涉及协程栈的切换(寄存器的读写),这些都是基于汇编实现的。低版本Go语言实际上没有逻辑处理器P,也就是说基于GM模型也能实现协程调度相关逻辑,那么逻辑处理器P到底有什么作用呢?
第2章将对GMP调度模型进行详细介绍。
(2)调度器
每一个线程M都有一个调度协程g0,该协程实现了协程调度功能。Go语言调度器有哪些途径可以获取可运行协程G呢?首先,每一个逻辑处理器P都有一个可运行协程队列,调度器通常只需要从当前逻辑处理器P的可运行协程队列获取协程。此外,为了避免多个逻辑处理器P负载分配不均衡,Go语言还有一个全局可运行协程队列,在某些条件下也会从全局可运行协程队列获取协程。
Go语言调度器也有时间片调度的概念。提到时间片,就不得不提到一个辅助线程,该线程会检测是否有协程执行时间过长,如果有,则会“通知”该协程让出CPU。在Go 1.14以下的版本中,Go语言基于协作的方式实现通知功能(需要主动检测,否则无法接收到通知,当然也就无法让出CPU),而在Go 1.14及以上的版本中,Go语言是基于信号实现通知功能的。
第2章将对Go语言调度器进行详细介绍。
(3)调度器触发时机
在Go语言中,很多场景都有可能触发调度,比如协程读写套接字导致的阻塞,协程读写管道导致的阻塞,协程抢锁导致的阻塞,协程休眠,协程执行了系统调用,以及抢占式调度等,这些场景都有可能导致该协程让出CPU,切换到调度程序。
第3章将对上述这些触发调度的情况进行详细介绍。
(4)并发编程
多协程同步是每一个Go开发者必须面对的问题。传统的多线程程序往往基于共享内存实现多线程同步,Go语言在此之上还为我们提供了基于管道-协程的CSP同步模型,这也是Go语言推荐的方案。
除此之外,Go语言还提供了其他几种多协程同步方案,包括基于锁的协程同步、并发散列表sync.Map、并发控制sync.WaitGroup、并发对象池sync.Pool、单实例sync.Once,以及非常有用的并发检测工具race等。
第4章将对Go语言并发编程的常用技巧进行详细介绍。
(5)GC
有了GC之后,我们不再需要关注内存的分配与释放。不过,在学习GC实现原理之前,需要了解Go语言是如何进行内存管理的,以及Go语言内存管理的基本单元(mspan)。
Go语言的GC是基于三色标记法实现的。整个GC流程可以简单划分为标记扫描、标记终止、未启动三个阶段。需要注意的是,GC也需要耗费CPU资源,甚至还会暂停所有用户协程(stopTheWorld)。因此,有时候可能需要进行一些GC调优方面的工作。不过,这可能需要你对GC的触发时机、GC协程的调度模式等有一定的了解。
第5章将对Go语言GC的实现原理进行详细介绍。
经过这一阶段的学习,相信你对Go语言的使用、底层原理等有了一定的了解,只是欠缺项目实战经验罢了。下一小节将重点介绍项目实战方面的学习路线。