多线程一词可以解释为多个控制线程或多个控制流。
虽然传统的 UNIX 进程包含单个控制线程,但多线程 (multithreading, MT) 会将一个进程分成许多执行线程,其中每个线程都可独立运行。本章介绍了一些多线程的术语和概念及其所产生的益处。
定义多线程术语
表 1–1 介绍了本书中所使用的一些术语。
表 1–1 多线程术语 术语 | 定义 |
---|---|
Process(进程) | 通过 fork(2) 系统调用创建的 UNIX 环境(如文件描述符和用户 ID 等),为运行程序而设置。 |
Thread(线程) | 在进程上下文中执行的指令序列。 |
POSIX pthread | 符合 POSIX 线程的线程接口。 |
Solaris thread(Solaris 线程) | 不符合 POSIX 线程的 Sun MicrosystemsTM 线程接口,pthread 的前序节点。 |
single-threaded(单线程) | 仅允许访问一个线程。 |
Multithreading(多线程) | 允许访问两个或多个线程。 |
User-level or Application-level thread(用户级线程或应用程序级线程) | 在用户空间(而非内核空间)中由线程调度例程管理的线程。 |
Lightweight process(轻量进程) | 用来执行内核代码和系统调用的内核线程,又称作 LWP。从 Solaris 9 开始,每个线程都有一个专用的 LWP。 |
Bound thread(绑定线程)(过时的术语) | 指的是在 Solaris 9 之前,和一个 LWP 永久绑定的用户级线程。从 Solaris 9 开始,每个线程都有一个专用的 LWP。 |
Unbound thread(非绑定线程)(过时的术语) | 指的是在 Solaris 9 之前,无须和一个 LWP 绑定的用户级线程。从 Solaris 9 开始,每个线程都有一个专用的 LWP。 |
Attribute object(属性对象) | 包含不透明数据类型和相关处理函数。这些数据类型和函数可以对 POSIX 线程一些可配置的方面,例如互斥锁 (mutex) 和条件变量,进行标准化。 |
Mutual exclusion lock(互斥锁) | 用来锁定和解除锁定对共享数据访问的函数。 |
Condition variable(条件变量) | 用来阻塞线程直到状态发生变化的函数。 |
Read-write lock(读写锁) | 可用于对共享数据进行多次只读访问的函数,但是要修改共享数据则必须以独占方式访问。 |
Counting semaphore(计数信号量) | 一种基于内存的同步机制。 |
Parallelism(并行性) | 如果至少有两个线程正在同时执行,则会出现此情况。 |
Concurrency(并发性) | 如果至少有两个线程正在进行,则会出现此情况。并发是一种更广义的并行性,其中可以包括分时这种形式的虚拟并行性。 |
符合多线程标准
多线程编程的概念至少可以回溯到二十世纪六十年代。多线程编程在 UNIX 系统中的发展是从八十年代中期开始的。虽然对多线程的定义以及对支持多线程所需要的功能存在共识,但是用于实现多线程的接口有很大不同。
在过去的几年内,POSIX(Portable Operating System Interface,可移植操作系统接口)1003.4a 工作小组一直致力于制定多线程编程标准。现在,该标准已得到认可。
该《多线程编程指南》基于 POSIX 标准 IEEE Std 1003.1 1996 版(又称作 ISO/IEC 9945–1 第二版)。最新修订版的 POSIX 标准 IEEE Std 1003.1:2001(又称作 ISO/IEC 9945:2002 和单一 UNIX 规范版本 3)中也提供了这些功能。
特定于 Solaris 线程的主题将在第 8 章,Solaris 线程编程中进行介绍。
多线程的益处
本节简要介绍多线程的益处。
在代码中实现多线程具有以下益处:
-
提高应用程序的响应
-
更有效地使用多处理器
-
改进程序结构
-
占用较少的系统资源
提高应用程序的响应
可以对任何一个包含许多相互独立的活动的程序进行重新设计,以便将每个活动定义为一个线程。例如,多线程 GUI 的用户不必等待一个活动完成即可启动另一个活动。
有效使用多处理器
通常,要求并发线程的应用程序无需考虑可用处理器的数量。使用额外的处理器可以明显提高应用程序的性能。
具有高度并行性的数值算法和数值应用程序(如矩阵乘法)在多处理器上通过多个线程实现时,运行速度会快得多。
改进程序结构
许多应用程序都以更有效的方式构造为多个独立或半独立的执行单元,而非整块的单个线程。多线程程序比单线程程序更能适应用户需求的变化。
占用较少的系统资源
如果两个或多个进程通过共享内存访问公用数据,则使用这些进程的程序可以实现对多个线程的控制。
但是,每个进程都有一个完整的地址空间和操作环境状态。每个进程用于创建和维护大量状态信息的成本,与一个线程相比,无论是在时间上还是空间上代价都更高。
此外,进程间所固有的独立性使得程序员需要花费很多精力来处理不同进程间线程的通信或者同步这些线程的操作。
结合线程和 RPC(远程过程调用)
通过将多个线程和一个远程过程调用 (remote procedure call, RPC) 结合起来,可以充分利用无共享内存的多处理器(如工作站集合)。这种结合将工作站集合视为一个多处理器,从而使应用程序的分布变得相对容易些。
例如,一个线程可以创建多个子线程,每个子线程随后可以请求远程过程调用,从而调用另一个工作站上的过程。尽管初始线程此时仅创建了一些并行运行的线程,但是这种并行性会涉及到其他计算机。
多线程概念
本节介绍多线程的基本概念。
并发性和并行性
在单个处理器的多线程进程中,处理器可以在线程之间切换执行资源,从而执行并发。
在共享内存的多处理器环境内的同一个多线程进程中,进程中的每个线程都可以在一个单独的处理器上并发运行,从而执行并行。如果进程中的线程数不超过处理器的数目,则线程的支持系统和操作环境可确保每个线程在不同的处理器上执行。例如,在线程数和处理器数目相同的矩阵乘法中,每个线程和每个处理器都会计算一行结果。
多线程结构一览
传统的 UNIX 已支持多线程的概念。每个进程都包含一个线程,因此对多个进程进行编程即是对多个线程进行编程。但是,进程同时也是一个地址空间,因此创建进程会涉及到创建新的地址空间。
创建线程比创建新进程成本低,因为新创建的线程使用的是当前进程的地址空间。相对于在进程之间切换,在线程之间进行切换所需的时间更少,因为后者不包括地址空间之间的切换。
在进程内部的线程间通信很简单,因为这些线程会共享所有内容,特别是地址空间。所以,一个线程生成的数据可以立即用于其他所有线程。
在 Solaris 9 和较早的 Solaris 发行版中,支持多线程的接口是通过特定的子例程库实现的。这些子例程库包括用于 POSIX 线程的 libpthread 和用于 Solaris 线程的 libthread。多线程通过将内核级资源和用户级资源分离来提供灵活性。在当前的发行版中,对于这两组接口的多线程支持是由标准 C 库提供的。
用户级线程
线程是多线程编程中的主编程接口。线程仅在进程内部是可见的,进程内部的线程会共享诸如地址空间、打开的文件等所有进程资源。
用户级线程状态
以下状态对于每个线程是唯一的。
-
线程 ID
-
寄存器状态(包括 PC 和栈指针)
-
栈
-
信号掩码
-
优先级
-
线程专用存储
由于线程可共享进程指令和大多数进程数据,因此一个线程对共享数据进行的更改对进程内其他线程是可见的。一个线程需要与同一个进程内的其他线程交互时,该线程可以在不涉及操作系统的情况下进行此操作。
注 –
顾名思义,用户级线程不同于内核级线程,只有系统程序员才能处理内核级线程。由于本书面向应用程序程序员,因此将不讨论内核级线程。
线程调度
POSIX 标准指定了三种调度策略:先入先出策略 (SCHED_FIFO)、循环策略 (SCHED_RR) 和自定义策略 (SCHED_OTHER)。SCHED_FIFO 是基于队列的调度程序,对于每个优先级都会使用不同的队列。SCHED_RR 与 FIFO 相似,不同的是前者的每个线程都有一个执行时间配额。
SCHED_FIFO 和 SCHED_RR 是对 POSIX Realtime 的扩展。SCHED_OTHER 是缺省的调度策略。
有关 SCHED_OTHER 策略的信息,请参见LWP 和调度类。
提供了两个调度范围:进程范围 (PTHREAD_SCOPE_PROCESS) 和系统范围 (PTHREAD_SCOPE_SYSTEM)。具有不同范围状态的线程可以在同一个系统甚至同一个进程中共存。进程范围只允许这种线程与同一进程中的其他线程争用资源,而系统范围则允许此类线程与系统内的其他所有线程争用资源。实际上,从 Solaris 9 发行版开始,系统就不再区分这两个范围。
线程取消
一个线程可以请求终止同一个进程中的其他任何线程。目标线程(要取消的线程)可以延后取消请求,并在该线程处理取消请求时执行特定于应用程序的清理操作。
通过 pthread 取消功能,可以对线程进行异步终止或延迟终止。异步取消可以随时发生,而延迟取消只能发生在所定义的点。延迟取消是缺省类型。
线程同步
使用同步功能,可以控制程序流并访问共享数据,从而并发执行多个线程。
共有四种同步模型:互斥锁、读写锁、条件变量和信号。
-
互斥锁仅允许每次使用一个线程来执行特定的部分代码或者访问特定数据。
-
读写锁允许对受保护的共享资源进行并发读取和独占写入。要修改资源,线程必须首先获取互斥写锁。只有释放所有的读锁之后,才允许使用互斥写锁。
-
条件变量会一直阻塞线程,直到特定的条件为真。
-
计数信号量通常用来协调对资源的访问。使用计数,可以限制访问某个信号的线程数量。达到指定的计数时,信号将阻塞。
使用 64 位体系结构
对于应用程序开发者,Solaris 64 位和 32 位环境的主要区别在于所使用的 C 语言数据类型的模型。64 位数据类型使用 LP64 模型,其中 long 和指针的宽度为 64 位,其他所有基础数据类型仍然与 32 位实现的数据类型相同。32 位数据类型使用 ILP32 模型,其中的 int、long 和指针宽度为 32 位。
以下简要概述了 64 位环境的主要特征以及使用该环境时的注意事项:
-
大虚拟地址空间
在 64 位环境中,进程的虚拟地址空间最高可达 64 位(即 18 EB)。目前,32 位进程的最大地址空间为 4 GB,较大的虚拟地址空间大约是其 40 亿倍。但是由于硬件限制,某些平台可能并不支持完整的 64 位地址空间。
大地址空间增加了可创建的具有缺省栈大小的线程数。在 32 位和 64 位系统中,栈的大小分别为 1 MB 和 2 MB。 在 32 位和 64 位系统中,具有缺省栈大小的线程数分别是大约 2000 个和 80000 亿个。
-
内核内存读取器
内核是在内部使用 64 位数据结构的 LP64 对象。这意味着,使用 libkvm、/dev/mem 或 /dev/kmem 的现有 32 位应用程序不能正常工作,必须转换为 64 位程序。
-
/proc 限制
使用 /proc 的 32 位程序可以查看 32 位进程,但是无法识别 64 位进程。用来描述进程的现有接口和数据结构不够大,因此无法包含 64 位值。对于此类程序,必须将其重新编译为 64 位程序,使其可同时适用于 32 位进程和 64 位进程。
-
64 位库
32 位库必须与 32 位应用程序进行链接,而 64 位库必须与 64 位应用程序进行链接。除已过时的库以外,所有的系统库都同时提供 32 位版本和 64 位版本。
-
64 位运算
64 位运算早已在以前的 32 位 Solaris 发行版中提供。现在,64 位实现提供了完整的 64 位计算机寄存器,用于进行整数运算和参数传递。
-
大文件
如果应用程序仅要求大文件支持,则可以保留 32 位并使用大文件接口。要充分利用 64 位功能,必须将应用程序转换为 64 位。
原文链接:http://zjf30366.blog.163.com/blog/static/4111645820089895151534/