线程和进程是程序员老生常谈的问题了,任何阶段的程序员都不敢轻视他。
事实上大部分程序员并没有系统化的学习过,也有很多人并没有机会好好运用它。所以,如果拉一个工作多年的程序员讨论,对方未必能说出个所以然。
本文是 Linux 下 C++ 多线程编程开发的系列文章之首,在介绍具体编程实现而言,先讲讲它的基础概念,并给予通俗化的解释,并在文章最后给出一个开放的思考题。
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。1
上面的定义来自于百度百科,定义的很准确,但同时也很抽象。
线程可一看作是轻量级的进程,它依赖于进程。
所以,搞清楚线程前,我们先来看看进程是什么。
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。2
上面的定义同样来自于百度百科,定义非常准确,但同时也非常抽象。
所以,为了很好的搞明白线程和进程,我尝试用一些通俗的比喻来解释。
我们把整个计算机比喻成一个公司,公司由一些基本的单元组成:
公司要正常运作,依赖于制度流程。
制度流程可以等同于什么呢?
操作系统
正因为规章制度流程的存在,公司的软硬件、设备资源可以协调给每一个部门。
如果公司十分庞大,组织复杂,那么部门就是最基础的单元。
也就是说
进程可以看做是部门
部门依法使用公司规定的软硬件资源,进程在操作系统的只能也是类似。
上面的定义中有讲到,现代操作系统中,进程是一个容器。
每个进程拥有自己的独立空间,相互间不干扰。
这个好理解,现实中,每个部门有自己的软硬件和员工,一般情况下相互不干涉。
那么线程呢?
线程依赖于进程,而不能独立存在。
线程是最基本的执行单元,对比现实生活就是是部门中的员工。
仔细体会一下。
部门提供资源,员工干活。
进程提供资源,线程干活。
但如果公司有重大决定,它一般不会直接针对个人,而是下发到部门,控制部门这一层就好了。
如果操作系统根据事情轻重缓急,它也会直接和进程交涉,进程受它的调度。
一个进程一般有一个或者多个线程。
同一个进程中的线程可以共享进程的数据。
但同时,其实线程也有自己的内存模型。
线程开发中可以利用 TLS(Thread-Local Storage,线程局部存储)来实现线程独有的内存数据不被同一个进程其它线程干扰,每一个线程维护一份共享变量的拷贝,所以基于拷贝上的操作不会影响其它线程。
那么,线程和进程有什么区别呢?
进程是资源分配的基本单位,线程是调度的基本单位。
这是一句名言,很好地概括了两者的区别。
用一句话来概括就是:
进程对应操作系统,线程对应 CPU。
我们常说的任务调度,其实通常讲 CPU 通过时间片轮转,调度线程。
也并不是说进程不能调度,是说线程更轻量化。
如果要了解更深入的话,可以带入到下一个问题。
我们用线程,提起的很多的就是多线程。
多线程的目的就是并行开发。
大家讲多线程的时候,总喜欢讲车站买票的例子。
窗口:@-@-@-@-@-@-@-@-@-@
现在有 10 个人买票。
如果这个工作效率很低,那么我们可以这样:
窗口:@-@-@-@-
窗口:@-@-
窗口:@-@-@-@-
增加类似的窗口,提供并行服务。
并行的目的是为了加速。
多线程开发也是基于上面 2 个原因:
并行就是,我要一边听歌,一边写文章。
加速就是,下载一个文件单线程太慢了,多个线程一起下载就能加速。
那是不是线程越多越好呢?
答案是否定的。
多线程能干的事情多进程也能干。
但两者都不是越多越好。
这涉及到上下文切换的问题。
上下文的英文单词是 Context。搞 Android 和 Java Web 开发的应该再熟悉不过了,它代表的是任务的同一语境。
上下文切换就是任务切换,可能是进程的切换也可能是线程的切换。
上下文的切换是一个很深的话题,我们大可不必在此过多讨论,我们可以简单看待:
上下文的切换就是任务状态的保存与恢复。
当 CPU 挂起一个任务时,它需要将 CPU 寄存器里面的信息保存到任务的堆栈当中,然后从下一个任务的堆栈中读取对应的 CPU 寄存器信息并恢复,那么下一个任务就可以执行了。
图片来源于网络3
但上下文切换的开销很昂贵,它有大量的工作需要处理,比如寄存器和内存页表的存储和恢复、内核数据结构的更新等等。
所以,线程并不是越多越好,因为线程越多,上下文切换越频繁,极端情况下效率不升反降。
并且,进程的上下文切换比线程开销的要大。
注意:在同一个进程中,线程的切换不会引起进程的切换,不同进程中,线程的切换会引起进程的切换。
所以,回到问题什么时候用进程什么时候用线程这个问题上来,一般认为:
2.相关性高的并行需求用线程
因为线程上下文切换开销小,所以同一个任务或者同一个业务逻辑的代码可以尝试用线程开发。
线程和进程的基本差别上面内容介绍的差不多了,下面提一些线程开发中常出现的基础概念。
用户态线程指的是用户层面自己创建的线程,自己管理生命周期,包括创建、切换、销毁。比如,最近流行的协程就属于此,一般通过线程库实现。
内核态线程指的是由操作系统的内核管理生命周期的线程,如用 pthread 创建的线程。
POSIX(Portable Operating System Interface of UNIX)是可移植操作系统接口,定义了操作系统应该为应用程序提供的接口标准。
什么意思呢?
操作系统有很多种,有些编程语言可以跨平台开发,有些则不能。
比如,Android 成立之初就选用 Java,原因就是 Java 跨平台,所以那些做 Java 开发的工程师可以很快投入到新的领域。但是 Java 的跨平台是建立在虚拟机上,虚拟机屏蔽了操作系统的不同,提供了统一的 API,但一定程度上牺牲了性能。我们很多年感觉的 Android 卡,和这有很大关系。
而 POSIX 意在获得操作系统源码级的 API 支持。
并且,POSIX 标准中定义了进程和线程相关标准。
Linux 是支持 POSIX 标准的,我们用 pthread 创建线程就属于此。
在 Linux 开发中,线程的实现可以通过系统调用。
但系统调用太麻烦了,一般用线程的封装好的线程库,目前常用的有:
文章最后,抛出一个问题。
我们知道,多线程开发是为了并行。
并行是通过 CPU 时间片调度而来。
线程 A: ++ + + +
线程 B: - - -- -
CPU : ++-+-+--+-
也有同学知道,所谓并行其实只是看起来并行。
那问题是:
可以串行的任务,非要人为并行吗?