线程详解
以下内容摘自《程序员的自我修养》
什么是线程?
线程(Thread),有时被称为轻量级(Lightweight Process, LWP),是程序执行流程的最小单元。一个标准的线程由线程ID、当前指令指针(PC)、寄存器集合和堆栈组成 。通常意义上,一个进程由一个到多个线程,各个线程之间共享程序的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开文件和信号)。一个经典的线程与进程的关系如图:
大多数软件应用中,线程的数量都不止一个。多个线程可以互不干扰地并发进行,并共享进程的全局变量和堆的数据。那么,多个线程与单线程的进程相比,又有哪些优势呢?通常来说,使用多线程的原因有如下几点:
- 某个操作可能会陷入长时间等待,等待的线程会进入睡眠状态,无法继续执行。多线程执行可以有效利用等待的时间。
- 某个操作(常常是计算)会消耗大量的时间,如果只有一个线程,程序和用户之间的交互会中断。多线程可以让一个线程负责交互,另一个线程负责计算。
- 程序逻辑本身要求并发操作,例如一个多端下载软件
- 多CPU或多核计算机,本身具备同时执行多个线程的能力,因此单线程程序无法全面发挥计算机的全部计算能力。
- 相对于多进程应用,多线程在数据共享方面的效率高得多。
线程访问权限
线程的访问非常自由,它可以访问进程内存里的所有数据,甚至包括其他线程的堆栈(如果它知道其他线程的堆栈地址,那么就是很少见的情况),但实际运用中线程也拥有自己的私有存储空间,包括以下几方面:
- 栈
- 线程局部存储
- 寄存器
线程调度与优先级
不论是在多处理器的计算机上还是单处理器的计算机上,线程总是“并发”执行的。当线程的数量小于等于处理器的数量时(并且操作系统支持多处理器),线程的并发是真正的并发,不同的线程运行在不同的处理器上,彼此之间互不相干。但对于线程数量大于处理器的情况,线程的并发会受到一些阻碍,因此此时至少一个处理器会运行多线程。
在单处理器对应多线程的情况下,并发是一种模拟出来的状态。操作系统会让这些多线程程序轮流执行,每次仅执行一小段时间,(通常是几十到几百毫秒),这样每一个线程就“看起来”在同时执行。这样的一个不断在处理器上切换不同的线程的行为称之为线程调度。在线程调度中,线程通常拥有三种状态,分别是:
- 运行(Running)
- 就绪(Ready)
- 等待(Waiting)
处于运行中的线程拥有一段可执行的时间,这段时间称为时间片。当时间片用尽后的时候,进程就开始等待某事件,那么它将进入等待状态。每当一个线程离开运行状态时,调度系统就会选择一个其他的就绪线程继续执行。在一个处于等待状态的线程锁等待的事件发生之后,该线程就将进入就绪状态;
线程调度自多任务操作系统问世以来,就不断被提出不同的方案和算法,现在主流的调度方式尽管各不相同,但都带有优先级调度和轮转法。
在windows中可以通过
BOOL WINAPI SetThreadPriority(HANDLE hThread, int nPriority);
来设置线程的优先级,而linux下与线程相关的操作可以通过pthread库来实现;
让我们总结一下,在优先级调度的环境下,线程的优先级改变一般有三种方式。
- 用户指定优先级
- 根据进入等待状态的频繁程序提升或降低优先级
- 长时间得不到执行而被提升优先级
可抢占线程和不可抢占线程
我们之前讨论的线程调度有一个特点,那就是线程在用尽时间片之后会被强制剥夺继续执行的权利,而进入就绪状态,这个过程叫做抢占。
在不可抢占的线程中,线程主动放弃执行无非是两种情况。
- 当线程试图等待某一事件时(I/O等)。
- 线程主动放弃时间片
因此,在不可抢占线程执行时候,有一个显著的特点,那就是线程调度的时间是确定的。线程调度只会发生在线程主动放弃执行或线程等待某事件的时候。这样可以避免一些因为抢占式线程里调度时间不确定而产生的问题。
Linux的多线程
Windows对进程和线程的实现如同教科书一般标准,windows内核有明确的线程和进程的概念,并且有一系列的API来操纵它们。但对于linux来说,线程并不是一个通用的概念。
Linux对多线程的支持颇为贫乏,事实上,在Linux内核中并不存在真正意义上的线程概念。Linux将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上不同任务之间都可以选择共享空间,因此在实际意义上,共享同一个内存空间的多个任务构成了一个进程,这些任务也就成了这个进程中的线程。在Linux下,用以下方法可以创建一个新的任务:
fork函数产生一个和当前进程完全意义的新进程,并和当前进程一样从fork函数里返回。例如如下代码:
pid_t pid; if (pid = fork()) { .... }
fork只能够产生本任务的镜像,因此须要使用exec配合才能启动别的新任务。exec可以用新的可执行映像替换当前的可执行镜像