你好,我是晁岳攀,网名鸟窝。之前我在微博研发平台架构中心担任资深架构师,同时也是微服务框架 rpcx 的作者,欢迎来到“Go 并发编程实战课”。

并发编程,为什么选 Go?

为什么要学 Go 并发呢?我想先和你聊聊我和 Go 结缘的经历。

作为一位老程序员,我在清华同方、摩托罗拉、Comcast 等公司,一直使用 Java 做项目开发。但是后来,我毅然抛弃了十几年的 Java 编程经验,投入到了 Go 语言的怀抱,为什么呢?

一句话,我被 Go 的简单高效所打动。它不仅部署方便,自带完善的工具链,特别是 Go 在处理并发场景上表现出的独特性能,更是让我着迷。

我们知道,Java 语言的编码非常繁琐,为了应用设计模式而做了大量的冗长设计,而 Go 就不一样了。它提供了便利的并发编程方式,简简单单的 Go 语句,就可以创建多个 goroutine 执行并发任务。而且,Go 还提供了独特的 Channel 类型,很容易实现 goroutine 之间的数据交流。所以,Go 并发编程入门很容易,即使是初学者,要写一个使用 goroutine 异步输出“Hello World”的例子,也可以不费吹灰之力。

不过,和其他语言相比,Go 微服务治理框架的发展还是比较晚的。当阿里出品的 Java 微服务框架 Dubbo 被广泛应用时,Go 生态圈还没有微服务框架。

于是,四五年前,为了填补 Go 生态圈微服务化的缺失,我就用 Go 开发了一个微服务的框架 rpcx。它既有类似标准 rpc 库的易用特点,又包含了非常丰富的服务治理的功能。而且,根据benchmark 测试,rpcx 有着数一数二的性能,很多互联网企业(比如马蜂窝、百度等)都在使用。

在微博的四年时间里,我使用 Go 参与开发多个基础架构系统,并负责中国版权链、微博下一代的 Redis 集群系统、数据库资源云等系统的设计和开发工作。在多年的实战中,我遇见过各种各样的并发难题,积累了大量的高并发高吞吐的服务器开发经验,也梳理了一整套并发编程的知识体系。

2019 年,astaxie(谢孟军)邀请我在 Gopher China 大会上做一个关于 Go 并发编程的分享。我准备了一份 120 页的PPT,全面地介绍了 Go 并发编程的基础内容,包括基本并发原语、扩展并发原语和 Channel 等。会后,现场的观众都说干货满满,希望我能提供无删改版的 PPT。

后来,在 Go 爱好者的强烈要求下,我又在滴滴举办了一场 Go 并发编程的培训,详细地分享了我的并发编程心得和经验,包括各种并发原语的基本用法和实现机制。

结合我自己的开发经验,以及这些年的技术分享经历,我真切地感受到了这一点:Go 并发编程的重要性不容置疑。只要是使用 Go 开发的大型应用程序,并发是必然要采用的技术。

但同时,我也了解到,很多人想要学习 Go 并发编程,却不知道该从何学起,也不知该如何精进。

学习 Go 并发编程,有哪些困难?

那学习 Go 并发会有哪些困难呢?我总结了一下,主要是有 5 大问题。

  1. 在面对并发难题时,感觉无从下手,不知道该用什么并发原语来解决问题
  2. 如果多个并发原语都可以解决问题,那么,究竟哪个是最优解呢?比如说是用互斥锁,还是用 Channel。
  3. 不知道如何编排并发任务。并发编程不像是传统的串行编程,程序的运行存在着很大的不确定性。这个时候,就会面临一个问题,怎么才能让相应的任务按照你设想的流程运行呢
  4. 有时候,按照正常理解的并发方式去实现的程序,结果莫名其妙就 panic 或者死锁了,排查起来非常困难
  5. 已知的并发原语都不能解决并发问题,程序写起来异常复杂,而且代码混乱,容易出错。

每一位刚入门 Go 的程序员,在深入学习 Go 语言的时候,尤其是面对 Go 并发编程的时候,都会遇到这些问题。那么,具体该怎么学呢?

怎么提升 Go 并发编程能力?

学习这件事儿,最怕的就是不成体系,即使知识点之间是彼此独立的,也必定存在着联系。我们要做的,就是找出逻辑关系,拎出知识线。我认为,关于 Go 并发编程,有两条主线,分别是知识主线和学习主线。具体是啥意思呢?可以看下面的这张知识地图。

从图中可以看到,在知识主线层面,这门课程的核心内容设计了 5 个模块:

  1. 基本并发原语:在这部分,我会介绍 Mutex、RWMutex、Waitgroup、Cond、Pool、Context 等标准库中的并发原语,这些都是传统的并发原语,在其它语言中也很常见,是我们在并发编程中常用的类型。
  2. **原子操作:**在这部分,我会介绍 Go 标准库中提供的原子操作。原子操作是其它并发原语的基础,学会了你就可以自己创造新的并发原语。
  3. Channel:Channel 类型是 Go 语言独特的类型,因为比较新,所以难以掌握。但是别怕,我会带你全方位地学习 Channel 类型,你不仅能掌握它的基本用法,而且还能掌握它的处理场景和应用模式,避免踩坑。
  4. 扩展并发原语:目前来看,Go 开发组不准备在标准库中扩充并发原语了,但是还有一些并发原语应用广泛,比如信号量、SingleFlight、循环栅栏、ErrGroup 等。掌握了它们,就可以在处理一些并发问题时,取得事半功倍的效果。
  5. 分布式并发原语:分布式并发原语是应对大规模的应用程序中并发问题的并发类型。我主要会介绍使用 etcd 实现的一些分布式并发原语,比如 Leader 选举、分布式互斥锁、分布式读写锁、分布式队列等,在处理分布式场景的并发问题时,特别有用。

沿着这条知识主线,我会带你建立起一个丰富的并发原语库。你可以把并发问题当成一个强大的敌人,而这些并发原语,就是我们的武器。每一种并发原语都有它的用处,你只有知道足够多的并发原语,才能灵活地应对各种场景。

那具体怎么掌握这些武器呢?课程的每一个模块都是独立的,它们之间没有任何依赖问题,你可以结合自己的实际情况,有重点地进行学习。如果你对 Channel 类型不是太熟悉,就可以先看 Channel 这个模块的内容;如果你已经非常熟悉标准库的并发原语了,就可以看看扩展并发原语和分布式并发原语的内容。

同时,在学习主线层面,主要是四大步骤,包括基础用法、实现原理、易错场景、知名项目中的 Bug。每一个模块,我都会带着你按照这四个步骤来学习,目的就是带你熟知每一种并发原语的实现机制和适用场景。

Go 中有一个大的方向,就是任务编排用 Channel,共享资源保护用传统并发原语。在刚开始学习时,你可以基于这个原则去选择相应的并发原语,这是没错的。但是,如果你想要在 Go 并发编程的道路上向前走,就不能局限于这个原则。

实际上,针对同一种场景,也许存在很多并发原语都适用的情况,但是一定是有最合适的那一个。所以,你必须非常清楚每种并发原语的实现机制和适用场景,千万不要被网上的一些文章误导,万事皆用 Channel。

而且,你还可以深入学习下 Go 并发原语的源代码。你会发现很多独到的设计,比如 Mutex 为了公平性考量的设计、sync.Map 为提升性能做的设计,以及很多并发原语的异常状况的处理方式。尤其是这些异常状况,常常是并发编程中程序 panic 的原因。

所以,如果你能深入了解这些并发原语的实现,不但会提高你的编程能力,还能让你避免在开发中踩并发问题的坑。这个时候,你就达到精通的程度了。

如果没有做过大型并发项目,你可能还不太清楚并发原语的重要性。那么,我建议你先阅读一下课程中介绍的知名项目中犯的错,这也是这门课里我特别设计的一部分内容。通过理解这些 Go 大牛们犯的错误以及解决方案,你就可以积累一套避坑指南和应对之道。

有了这两条线的学习,我们就从广度和深度上掌握了 Go 并发编程的知识点。这些是不是就足够了呢?我们还可以更进一步,你要有野心能够创造出自己需要的并发原语。

这里的创造有两层含义。第一层是对既有的并发原语进行组合,使用两个、三个或者更多的并发原语去解决问题。比如说,我们可以通过信号量和 WaitGroup 组合成一个新的并发原语,这个并发原语可以使用有限个 goroutine 并发处理子任务。第二层含义是“无中生有”,根据已经掌握的并发原语的设计经验,创造出合适的新的并发原语,以应对一些特殊的并发问题。比如说,标准库中并没有信号量,你可以自己创造出这个类型。

达到了这一层,那就不得了了,可以说你对 Go 并发原语的掌握已经出神入化了。那想要达到这个程度是不是很难呢?确实不容易,不过我相信,如果你仔细学习了我们课程里的每一节课,心里牢牢地锚定 3 个目标:建立起一个丰富的并发原语库;熟知每一种并发原语的实现机制和适用场景;能够创造出自己需要的并发原语。达到了这 3 个目标,你就可以轻松地应对各种并发问题了。甚至可以说,你几乎能站在 Go 并发编程的顶端,成为大牛中的一员。

最后,我想说的是,Go 并发编程的世界确实纷繁复杂,涉及到的内容非常多。你可以把它看作是一个江湖,如果你想拥有极强的作战力,就要拥有足够多的武器,并且修炼内功。这门课,就是你的修炼山洞,我准备了应有尽有的宝藏,等待着你来挖掘。

修炼的过程中,最好有人和你并肩而行,共同成长。欢迎你把这门课分享给你的朋友或同事,和他 / 她一起提升并发编程的功力。