《程序员的自我修养》- 线程
线程在哪里
线程可以理解为进程的一部分。比如我现在编写了一个C语言程序,其中用到pthread.h中的函数实现了多线程的功能。然后我将这个程序编译为可执行文件,当我运行它时,我相当于创建了一个进程,这个进程拥有自己的虚拟地址空间,而进程的线程就存在于虚拟地址空间的用户空间中,各个线程共享进程的内存空间。
定义,组成与作用
- 定义:线程是程序执行流的最小单元。
- 组成:线程由线程ID(TID),当前指令指针(PC),寄存器集合和堆栈组成。
- 作用:可以简单抽象为非阻塞,并发或并行,性能,共享。
下面是对上面几项的解释与理解:
- 定义可以和组成结合起来理解,这三点构成了一个最小程序执行流,称为一个线程。
- 一个程序执行流需要一个独立的存储空间来保证正常运行,即一部分寄存器和堆栈(线程理论上拥有对进程用户空间的所有数据,包括其他线程的数据,但其实线程也拥有完全私有的存储空间)。
- 程序执行流所谓“流”即一个流程,需要知到当前执行到的位置,即程序计数器(PC)。在32位Intel处理器中,该计数器的功能由寄存器EIP来承担。
- 线程ID正如其名,是每一个线程唯一的标识,它在资源分配等方面起着重要的作用。
- 线程的作用:
- 非阻塞是指当某一个线程遇到需要等待或者极计算量较大,耗时较长的问题时,其他线程可以继续运行下去,而不是必须等待该线程完成工作。如带有用户界面的大规模计算程序需要快速对用户的操作作出反应,不需要等待负责计算的线程。
- 并发与并行主要影响体现在网络服务器上,线程使得同时处理多个用户的请求成为可能。
- 对于多核CPU来说,程序可以通过按一定方式配置多线程来实现对性能的最大化利用,从而提高程序的运行效率,这也是服务器用的核心数几十甚至上百的CPU的游戏表现并不突出的原因:游戏一般不会适配过多的CPU核心数,从而造成了极大的资源浪费。
- 多线程之间可以以较高的效率实现数据的共享。
并发与并行并不相同:并发是指CPU同一时间只执行一项任务,只是任务之间切换的速度极快,以至于造成多个任务同时进行的假象。而并行则是货真价实的多任务同时进行,但它依赖于CPU硬件方面的支持(核心数)
线程的调度与优先级
线程在调度中拥有三种状态,分别是:
- 运行:线程正在执行
- 就绪:此时线程可以立即执行,但是CPU被占用
- 等待:此时线程正在等待某一事件发生,无法执行
这三种状态之间的相互转换可以用下面这张图来解释: 解释一下图中出现的部分名词的意思:
- 时间片:处于运行中的线程所拥有的一段可以执行的时间。
- 等待:运行中的线程停止执行,在时间片用尽前脱离运行状态。
- IO密集型线程:由于线程进入等待状态一般是为了等待IO时间,如用户输入,因此频繁等待的线程被称为IO密集型线程。
- CPU密集型线程:一般需要大量计算的线程等待次数较少,故称为CPU密集型线程。
- 优先级调度:操作系统根据线程的不同优先级来进行调度的方式,拥有更高优先级的线程会更早执行。其实现代的操作系统调度方式可能更多。更复杂,此处仅仅是一个较为典型的例子。
在线程的调度中,还有一种少见的线程,它们即使是在时间片用尽的情况写也依然保持运行,除非该线程进入等待状态或者主动放弃执行,这种线程被称为不可抢占式线程。这样做的其中一个目的是保证线程所处理的数据的一致性,即防止其他线程抢占后修改数据,
Linux下的多线程
不同于Windows,早期Linux下的线程概念并不明确。在Linux内核中,进程和线程都称为任务,每一个任务都类似于一个单一的进程。但Linux中的任务可以共享内存空间,因此共享内存空间的多个任务可以从概念上认为是线程。如可以进行资源共享操作的系统调用clone便可以创建出概念上的进程。 下面是一个例子:
1 |
|
需要注意当参数为vm时才开启内存共享的选项,这意味着clone所产生的是一个概念上的线程,但如果不开启该选项的话,产生的就是一个进程。 下面我们来看一下程序的执行结果:
可以看到,相比较于左边创建了新进程的程序,右边创建新线程的程序具有明显的数据共享的特征。而且从右边的输出结果中,我们也可以更清晰地看到操作系统对线程的调度(即轮换执行)的结果。 然而在多线程的程序中,如果某一个数据对于多个线程来说都至关重要,那么该如何去处理呢。这就是之后要提到的线程安全的内容了。 虽然clone函数创建出的只是概念上的进程,但其实在用户层面上已经出现了符合POSIX标准的线程库,可以用以下方式查看:
1 |
|