今天,我们来聊聊“并发”(Concurrency)、“多线程”(multithreading)。
在 20 年前,大多数人(当然也包括我)对这两个词还是十分陌生的。那个时候,CPU 的性能不高,要做的事情也比较少,没什么并发的需求,简单的单进程、单线程就能够解决大多数问题。
但到了现在,计算机硬件飞速发展,不仅主频上 G,还有了多核心,运算能力大幅度提升,只使用单线程很难“喂饱”CPU。而且,随着互联网、大数据、音频视频处理等新需求的不断涌现,运算量也越来越大。这些软硬件上的变化迫使“并发”“多线程”成为了每个技术人都不得不面对的课题。
通俗地说,“并发”是指在一个时间段里有多个操作在同时进行,与“多线程”并不是一回事。
并发有很多种实现方式,而多线程只是其中最常用的一种手段。不过,因为多线程已经有了很多年的实际应用,也有很多研究成果、应用模式和成熟的软硬件支持,所以,对这两者的区分一般也不太严格,下面我主要来谈多线程。
认识线程和多线程
要掌握多线程,就要先了解线程(thread)。
线程的概念可以分成好几个层次,从 CPU、操作系统等不同的角度看,它的定义也不同。今天,我们单从语言的角度来看线程。
在 C++ 语言里,线程就是一个能够独立运行的函数。比如你写一个 lambda 表达式,就可以让它在线程里跑起来:
auto f = []() // 定义一个lambda表达式
{
cout << "tid=" <<
this_thread::get_id() << endl;
};
thread t(f); // 启动一个线程,运行函数f
任何程序一开始就有一个主线程,它从 main() 开始运行。主线程可以调用接口函数,创建出子线程。子线程会立即脱离主线程的控制流程,单独运行,但共享主线程的数据。程序创建出多个子线程,执行多个不同的函数,也就成了多线程。
多线程的好处你肯定能列出好几条,比如任务并行、避免 I/O 阻塞、充分利用 CPU、提高用户界面响应速度,等等。
不过,多线程也对程序员的思维、能力提出了极大的挑战。不夸张地说,它带来的麻烦可能要比好处更多。
这个问题相信你也很清楚,随手就能数出几个来,比如同步、死锁、数据竞争、系统调度开销等……每个写过实际多线程应用的人,可能都有“一肚子的苦水”。
其实,多线程编程这件事“说难也不难,说不难也难”。这句话听上去好像有点自相矛盾,但却有一定的道理。为什么这么说呢?
说它不难,是因为线程本身的概念是很简单的,只要规划好要做的工作,不与外部有过多的竞争读写,很容易就能避开“坑”,充分利用多线程,“跑满”CPU。
说它难,则是因为现实的业务往往非常复杂,很难做到完美的解耦。一旦线程之间有共享数据的需求,麻烦就接踵而至,因为要考虑各种情况、用各种手段去同步数据。随着线程数量的增加,复杂程度会以几何量级攀升,一不小心就可能会导致灾难性的后果。
多线程涵盖的知识点太多,许多大师、高手都不敢自称精通,想用一节课把多线程开发说清楚是完全不可能的。
所以,今天我们只聚焦 C++ 的标准库,了解下标准库为多线程编程提供了哪些工具,在语言层面怎么改善多