参考:"Operating System Concepts, Ninth Edition ", Chapter 4
概述
- 线程是 CPU 使用的基本单位,由
程序计数器
、堆栈
、一组寄存器
和一个线程 ID
组成 - 传统(重量级)进程有一个控制线程 - 有一个程序计数器和一个可以在任何给定时间执行的指令序列。
- 多线程应用程序在单个进程中有多个线程;每个都有自己的程序计数器、堆栈和一组寄存器,但共享公共代码、数据和某些结构
单线程进程 和 多线程进程
- 使用线程的原因
- 每当一个进程有多个任务需要独立执行,线程在现代编程中就非常有用了。
- 当其中一个任务可能阻塞时尤其如此,并且希望允许其他任务继续进行而不阻塞
- 例如,在文字处理器中,后台线程可能会检查拼写和语法,而前台线程会处理用户输入(击键),第三个线程从硬盘驱动器加载图像,第四个线程定期自动备份正在编辑的文件。
- 另一个例子是 Web 服务器 - 多个线程允许同时满足多个请求,而不必按顺序处理请求或为每个传入请求分叉单独的进程。
- 使用线程的优势
- 响应性 —— 一个线程可以提供快速响应,而其他线程被阻塞或减慢执行密集计算。
- 资源共享 —— 默认情况下,线程共享公共代码、数据和其他资源,这允许在单个地址空间中同时执行多个任务。
- 经济 —— 创建和管理线程(以及它们之间的上下文切换)比为进程执行相同任务要快得多。
- 可扩展性,即多处理器体系结构的利用 ——
一个单线程进程只能在一个 CPU 上运行,无论有多少 CPU 可用
,而多线程应用程序的执行可能会在可用处理器之间拆分
多核心编程
- 计算机体系结构的最新趋势是生产具有多核的芯片,或单芯片上的多 CPU。
- 在多核芯片上,线程可以分布在可用的内核上,从而实现真正的并行处理
- 对于操作系统,多核芯片需要新的
调度算法
来更好地利用可用的多核。 - 随着多线程变得越来越普遍和越来越重要(数千个而不是数十个线程),CPU 已被开发的在硬件层面每个内核能支持更多的并发线程。
- 编程挑战
识别任
务 - 检查应用程序以查找可以同时执行的活动平衡
- 寻找提供同等价值的同时运行的任务,也就是说不要在琐碎的任务上浪费线程。数据拆分
- 防止线程相互干扰。数据依赖性
——如果一项任务依赖于另一项任务的结果,则需要同步任务以确保以正确的顺序访问测试和调试
- 在并行处理情况下本质上更加困难,因为竞争条件变得更加复杂且难以识别。
- 并行类型
- Data parallelism 在多个核心(线程)之间划分数据,并对数据的每个子集执行相同的任务。
- Task parallelism 任务并行性将要在不同内核之间执行的不同任务划分并同时执行
在实践中,从来没有一个程序单独被其中一个或另一个划分,而是由某种混合组合来划分。
多线程模型
- 现代系统中有两种类型的线程需要管理:
用户线程
和内核线程
- 内核之上支持用户线程,没有内核支持。这些是应用程序程序员将放入他们的程序中的线程。
- 操作系统本身的内核支持
内核线程
。所有现代操作系统都支持内核级线程,允许内核同时执行多个任务和/或同时服务多个内核系统调用。 - 在具体实现中,必须使用以下策略之一将用户线程映射到内核线程。
-
多对一模型
-
一对一模型
-
多对多模型
- 多对多模型将任意数量的用户线程多路复用到数量相等或更少的内核线程上,结合了一对一和多对一模型的最佳特性。
- 用户对创建的线程数没有限制
- 阻塞内核系统调用不会阻塞整个进程。
- 进程可以跨多个处理器拆分
- 根据存在的 CPU 数量和其他因素,可以为各个进程分配可变数量的内核线程。
Thread Libraries
- 线程库为程序员提供了用于创建和管理线程的 API。
- 线程库可以在用户空间或内核空间中实现。前者涉及仅在用户空间内实现的 API 函数,没有内核支持。后者涉及系统调用,并且需要具有线程库支持的内核。
- 目前使用的三个主要线程库
- POSIX Pthreads - 可以作为用户或内核库提供,作为 POSIX 标准的扩展。
- Win32 threads 在 Windows 系统上作为内核级库提供。
- Java threads 线程的实现基于 JVM 运行的任何操作系统和硬件
- Pthreads
- POSIX 标准 (IEEE 1003.1c) 定义了 pThreads 的规范,而不是实现。
- pThreads 可在 Solaris、Linux、Mac OSX、Tru64 和 Windows 的公共域共享软件上使用
- 全局变量在所有线程之间共享。
- 一个线程可以等待其他线程重新加入,然后再继续。
- pThreads 在指定函数中开始执行
- Windows Threads
- Java Thread
隐式线程
- 线程池
- 每次需要创建新线程,然后在完成时将其删除可能效率低下,并且还可能导致创建大量(无限)线程。
- 另一种解决方案是在进程首次启动时创建多个线程,并将这些线程放入线程池中
- 线程根据需要从池中分配,不再需要时返回池中。
- 当池中没有可用的线程时,进程可能必须等待直到有一个可用。
- 线程池中可用的(最大)线程数可以由可调整的参数确定,可能动态地响应不断变化的系统负载。
- Win32 通过“PoolFunction”函数提供线程池
- Java 通过 java.util.concurrent 包提供对线程池的支持
- Apple 在 Grand Central Dispatch 架构下支持线程池
- OpenMP
OpenMP 是一组可用于 C、C++ 或 FORTRAN 程序的编译器指令,它们指示编译器在适当的情况下自动生成并行代码。
- Grand Central Dispatch
GCD 是 Apple 的 OSX 和 iOS 操作系统上可用的 C 和 C++ 的扩展,以支持并行性。
线程问题
- fork() 和 exec() 系统调用
如果一个线程分叉,是复制整个进程,还是新进程是单线程的?
如果新进程立即执行,则无需复制所有其他线程。如果没有,则应复制整个过程。
许多版本的 UNIX 为此提供了多个版本的 fork 调用。
- 信号处理