后台开发必备技能:多线程编程

发表时间: 2019-09-29 14:08

这里先给大家分享一个小故事:

在我刚开始参加工作的那年,公司安排我开发一款即时通讯软件(IM,类似于 QQ 聊天软件),在这之前我心里也知道如果多线程操作一个整型值是要加锁的,但是当时为了图代码简便,而且在实际调试的时候,没有加锁的代码也从来没出过问题。于是我就心存侥幸了,觉得对整型值加锁真是多此一举。

我们的软件有类似于 QQ 这种单人聊天功能,每个用户都有一个整型的 userid,问题就出在这里。当时公司的老板和他妻子也使用这款软件,问题来了:有一天早上老板在这个软件上给他妻子发了一段亲密的话。问题来了,由于多线程操作他妻子的 userid 没加锁,最终变成了另外一个人的 userid,而这个 userid 恰好是我的账户。于是老板发给他妻子的聊天内容就被发给我了

我当时看到聊天内容很奇怪,还回复了他一句,并且还带上了我自己的姓名……事情的结果,可想而知了,老板非常尴尬也非常生气……从那以后,老板看我的眼神都是怪怪的。我自知理亏,再也不侥幸了,凡是多线程读写整型变量都养成加锁的好习惯

这是我曾经犯过的错误,也就是所谓的线程安全问题。“年轻”时的我,当时不明白为什么一个整型变量在多线程操作时存在安全问题,进而引起业务上的错乱问题。

很多年以前的时候,技术面试的时候面试官经常会问“程序什么时候需要开启新的线程”这样的问题,那个时候多核 CPU 才刚开始普及,很多人也是才开始逐渐接触多线程技术。而如今多核 CPU 和多线程编程技术已经是下里巴人的技术了。

因此“程序什么时候需要开启新的线程”这一类问题,简单解释一下,就是为了提高解决问题的效率,毕竟大多数情况下,多个 CPU 并行做一件事总比单个 CPU 做要快很多。

然而,多线程程序虽然强大,但也让原来的程序执行流程变得复杂和具有一定的不确定性,比如带来资源的竞态问题,初学者或许意识不到带来的后果,往往不能够很好处理这个问题,帮助学习多线程编程的初中级读者,理清多线程程序的执行原理、脉络是我写专栏的创作初衷之一。

由于各种库和运行环境对操作系统多线程接口的封装,很多技术开发者做了很多年的开发,仍然只是个界面或者仅知道调用库的“业务”程序员,他们只能面向搜索引擎编程,遇到稍微复杂一点的多线程逻辑就不知如何下手了。

专栏将从操作系统原理的角度介绍多线程技术的方方面面,从基础的知识到高级进阶,它们是笔者这些年工作的经验总结和踩坑之后的教训。

专栏的内容主要分为以下三大方面。

操作系统层面上关于多线程和多线程协作的接口原理

无论是哪种编程语言和库,其最终都是要运行在操作系统上的,应用程序本身的特性应该是决定采用何种编程工具的最主要因素,但是无论你采用什么样的编程语言,通过了解操作系统 API 从而深入理解操作系统的工作原理,这本身就有很重要的意义。

操作系统是一个非常复杂的系统,在 API 之上加一层编程语言并不能消除其复杂性,最多不过是把复杂性隐藏起来而已。说不定什么时候,复杂的那一面迟早会蹦出来拖你的后腿,懂得系统 API 能让你可以更快地挣脱困境。

因此,针对多线程编程,本专栏将详细地介绍 Windows 和 Linux 操作系统层面上提供的各种多线程接口,理解并熟悉它们的使用后,读者在接触或者学习其他语言或者库提供的多线程功能时,可以快速地上手和掌握。

以协程这一技术为例,虽然协程是计算机操作系统原理之一,但是我们所接触的大多数操作系统并没有从系统层面上支持协程这一技术

而像 Golang 这一类语言是提供协程功能的,那这一类语言是如何支持的?如果你对操作系统的线程有着深入的了解,你也不难想明白:

线程是操作系统的内核对象,当多线程编程时,如果线程数过多,就会导致频繁地上下文切换,这些对性能是一种额外的损耗。

协程,是在应用层模拟的线程,它避免了上下文切换的额外损耗,同时又兼顾了多线程的优点,简化了高并发程序的复杂度。

再例如,线程局部存储技术是我们常用的一项多线程技术,它的存在让每个线程可以有自己私有存放数据的空间。那线程局部存储技术是如何实现的呢?本专栏中会庖丁解牛地介绍线程局部存储是什么以及它的实现原理。

基于操作系统多线程理论衍生出来的一些扩展理论模型和应用

正因为存在多线程编程,所以有了线程池模型,据我了解,“线程池”的实现和原理应该是多线程编程新手问的、聊的最多的一个技术点了。

专栏会带领读者利用各种操作系统提供的线程同步对象来实现一个线程池,进而引出生产者消费者理论模型,再进一步升华,引出所谓的消息中间件,如 Kafka、RabbitMQ。

对于技术方案,我们不推崇重复造轮子,但是一定要具有重复造轮子的能力和了解轮子的制造原理。有了这项能力之后,在使用一些开源的消息中间件时,我们因为“知其然、知其所以然”才会把这些软件在项目中用得更好。

专栏中会详细地介绍多线程操作整型变量非线程安全的原因以及解决方案。掌握了这些,你在学习像 Java 语言时,就明白了为什么 JDK 在操作一个整型变量时提供 AtomicInteger 这样的类了。

和多线程相关的,一些实际开发中的技巧和经验

如果你是一名开发者,那么曾经或许会为下面一些问题而头痛过,这些问题或许你在面试时被面试官问到或者在实际开发中遇到过:

  • 进程的 CPU 使用率过高如何查找原因并解决?

  • 如何让一个程序只允许使用者运行一个实例?

  • 在实际开发中,避免死锁有哪些可以遵循的规则?

  • 什么是条件变量的虚假唤醒?虚假唤醒会带来什么问题?如何解决?

  • 如何设计高效的线程池和队列模型?

  • 如何在线程函数中访问类的成员变量和函数?

诸如此类实际开发中经常遇到问题,实在太多了,这里就不一一列举了。本专栏在保持主干脉络介绍的同时,也会穿插介绍一些与多线程相关的开发技巧和经验。

当然,多线程问题本来就比较复杂,尤其是本专栏同时介绍 Windows 和 Linux 两个操作系统平台的接口,在实际编写程序时,由于操作系统提供的 API 不一样,为了跨平台,我们不得不写许多跨平台代码。好在,C++ 11/14 标准给 C++ 引入了大量的多线程类和库,本专栏也会详尽地介绍它们的用法。

扫码查看《C/C++多线程编程精髓》专栏详情

以下是专栏的主要内容导图:

内容亮点:

  • 解析操作系统 API 层的多线程编程原理

  • 展示 Windows 和 Linux 操作系统的基本原理

  • 讲解线程间各种同步原语的适用场景、优缺点

  • 贯穿实际开发中的问题定位与排查

你将获得:

  • 彻底掌握多线程编程原理和编码经验

  • Windows 和 Linux 操作系统的基本原理

  • 线程之间各种同步原语的适用场景和优缺点

  • 相关编程惯用法和手段技巧

  • 了解 Java、Go 等上层语言运行时环境提供的功能是如何基于操作系统 API 实现的

  • 实际开发中一些问题定位与排查


由于操作系统调度线程时的不确定性,同样的逻辑可能在不同机器、不同时刻有不同的行为表现,也因此增加了排查和定位问题的难度。这是在学习和开发多线程程序时不得不面临的问题。

建议读者将专栏各个章节中的代码示例都实际在机器上运行一遍,认真、准确地理解每一个多线程同步原语的适用场景和性能优缺点。只要透彻地理解了这些操作系统提供的基础多线程同步原语,在面对它们的衍生物(如线程池、消息队列、协程技术等)时可以更快地学习和用好。

操作系统提供的 API 接口一般在相当长的时间内会保持不变的(至少保持向后兼容),一经学会,终生受用。在新技术新思想层出不穷、令人应接不暇时,掌握了操作系统层面的 API 和其设计思想原理,可以以不变应万变

最后,多线程编程在现代软件开发中是如此的重要,以至于熟练使用多线程编程是一名合格的后台开发人员的基本功,它是如此的重要,希望本专栏能帮助你掌握它,愿它能让你彻底告别多线程编程烦恼。