POSIX(Portable Operating System Interface of Unix)是一个操作系统接口,因而遵循(兼容)这一标准的操作系统都应该提供对POSIX线程的支持。
多线程一词可以解释为多个控制线程或多个控制流。
一、多线程的益处
在代码中实现多线程具有以下益处:
- 提高应用程序的响应:可以对任何一个包含许多相互独立的活动的程序进行重新设计,以便将每个活动定义为一个线程。例如,多线程 GUI 的用户不必等待一个活动完成即可启动另一个活动。
- 有效使用多处理器:通常,要求并发线程的应用程序无需考虑可用处理器的数量。使用额外的处理器可以明显提高应用程序的性能。具有高度并行性的数值算法和数值应用程序(如矩阵乘法)在多处理器上通过多个线程实现时,运行速度会快得多。
- 改进程序结构:许多应用程序都以更有效的方式构造为多个独立或半独立的执行单元,而非整块的单个线程。多线程程序比单线程程序更能适应用户需求的变化。
- 占用较少的系统资源:如果两个或多个进程通过共享内存访问公用数据,则使用这些进程的程序可以实现对多个线程的控制。但是,每个进程都有一个完整的地址空间和操作环境状态。每个进程用于创建和维护大量状态信息的成本,与一个线程相比,无论是在时间上还是空间上代价都更高。此外,进程间所固有的独立性使得程序员需要花费很多精力来处理不同进程间线程的通信或者同步这些线程的操作。
二、多线程概念
1.并发性和并行性
在单个处理器的多线程进程中,处理器可以在线程之间切换执行资源,从而执行并发。
在共享内存的多处理器环境内的同一个多线程进程中,进程中的每个线程都可以在一个单独的处理器上并发运行,从而执行并行。如果进程中的线程数不超过处理器的数目,则线程的支持系统和操作环境可确保每个线程在不同的处理器上执行。例如,在线程数和处理器数目相同的矩阵乘法中,每个线程和每个处理器都会计算一行结果。
事实上传统的UNIX已支持多线程的概念。每个进程都包含一个线程,因此对多个进程进行编程即是对多个线程进行编程。但是,进程同时也是一个地址空间,因此创建进程会涉及到创建新的地址空间。
创建线程比创建新进程成本低,因为新创建的线程使用的是当前进程的地址空间。相对于在进程之间切换,在线程之间进行切换所需的时间更少,因为后者不包括地址空间之间的切换。
在进程内部的线程间通信很简单,因为这些线程会共享所有内容,特别是地址空间。所以,一个线程生成的数据可以立即用于其他所有线程。
2.用户级线程
线程是多线程编程中的主编程接口。线程仅在进程内部是可见的,进程内部的线程会共享诸如地址空间、打开的文件等所有进程资源。
以下状态对于每个线程是唯一的。
- 线程 ID
- 寄存器状态(包括 PC 和栈指针)
- 栈
- 信号掩码
- 优先级
- 线程专用存储
由于线程可共享进程指令和大多数进程数据,因此一个线程对共享数据进行的更改对进程内其他线程是可见的。一个线程需要与同一个进程内的其他线程交互时,该线程可以在不涉及操作系统的情况下进行此操作。
3.线程调度
POSIX 标准指定了三种调度策略:
- 先入先出策略 (SCHED_FIFO):是基于队列的调度程序,对于每个优先级都会使用不同的队列。
- 循环策略 (SCHED_RR):与 FIFO 相似,不同的是前者的每个线程都有一个执行时间配额。
- 自定义策略 (SCHED_OTHER):缺省的调度策略,这个取决于系统的实现,大多数系统中它指的是分时调度
两个调度范围:进程范围 (PTHREAD_SCOPE_PROCESS) 和系统范围(PTHREAD_SCOPE_SYSTEM)。具有不同范围状态的线程可以在同一个系统甚至同一个进程中共存。进程范围只允许这种线程与同一进程中的其他线程争用资源,而系统范围则允许此类线程与系统内的其他所有线程争用资源。
4.线程取消
一个线程可以请求终止同一个进程中的其他任何线程。目标线程(要取消的线程)可以延后取消请求,并在该线程处理取消请求时执行特定于应用程序的清理操作。通过 pthread 取消功能,可以对线程进行异步终止或延迟终止。异步取消可以随时发生,而延迟取消只能发生在所定义的点。延迟取消是缺省类型。
5.线程同步
使用同步功能,可以控制程序流并访问共享数据,从而并发执行多个线程。
共有四种同步模型:
- 互斥锁:互斥锁仅允许每次使用一个线程来执行特定的部分代码或者访问特定数据。
- 读写锁:读写锁允许对受保护的共享资源进行并发读取和独占写入。要修改资源,线程必须首先获取互斥写锁。只有释放所有的读锁之后,才允许使用互斥写锁。
- 条件变量:条件变量会一直阻塞线程,直到特定的条件为真。
- 信号量:计数信号量通常用来协调对资源的访问。使用计数,可以限制访问某个信号的线程数量。达到指定的计数时,信号量将阻塞。
三、API
1.创建线程
#include
int pthread_create(pthread_t *tid, const pthread_attr_t *tattr,void*(*start_routine)(void *), void *arg); 调用成功完成则返回零,其它值表示出错
如果属性对象tattr为NULL,系统会创建具有以下属性的缺省线程:
还可以用 pthread_attr_init() 创建缺省属性对象,然后使用该属性对象来创建缺省线程。
start_routine 是新线程最先执行的函数。当start_routine 返回时,该线程将退出,其退出状态设置为由start_routine 返回的值。
当 pthread_create() 成功时,所创建线程的 ID 被存储在由tid 指向的位置中。
2. 等待线程终止
#include
int pthread_join(thread_t tid, void **status); 调用成功完成则返回零,其它值表示出错
该函数会一直阻塞调用线程,直到指定的线程终止。
指定的线程必须位于当前的进程中,而且不得是分离线程。如果status不为NULL时,则在pthread_join()成功返回时,指定线程的退出状态会通过它返回。
如果多个线程等待同一个线程终止,则所有等待线程将一直等到目标线程终止。然后,一个等待线程成功返回。其余的等待线程将失败并返回 ESRCH 错误。
在pthread_join()返回之后,应用程序可回收与已终止线程关联的任何数据存储空间。
3.分离线程
#include
int pthread_detach(thread_t tid); 调用成功完成则返回零,其它值表示出错
该函数是 pthread_join 的替代函数,可回收创建时 detachstate 属性设置为PTHREAD_CREATE_JOINABLE 的线程的存储空间。
pthread_detach()函数用于指示应用程序在线程tid终止时回收其存储空间。如果tid尚未终止,pthread_detach()不会终止该线程。
4.线程特定数据
线程可以有自己的专有数据,线程专有数据需要通过特殊的API创建和使用。
线程特定数据与全局数据非常相似,区别在于前者为线程专有。
线程特定数据基于每线程进行维护。每个线程特定数据项都与一个作用于进程内所有线程的键关联。通过使用key,线程可以访问自己和这个key相关的专有数据。
1)为线程特定数据创建键
#include
int pthread_key_create(pthread_key_t *key, void (*destructor) (void *)); 调用成功完成则返回零,其它值表示出错
pthread_key_create分配用于标识进程中线程特定数据的键。键对进程中的所有线程来说是全局的。创建线程特定数据时,所有线程最初都具有与该键关联的NULL值。
在使用各个键之前,要先对其调用一次pthread_key_create()。
创建键之后,每个线程都会将一个值绑定到该键。这些值特定于线程并且针对每个线程单独维护。如果创建该键时指定了destructor函数,则任意一个线程终止时,该函数都会被自动调用,传递给它的参数是该线程中与该键相关联的值。
当pthread_key_create成功返回时,会将已分配的键存储在key中。对该键的存储和访问必须进行正确的同步。
2)删除线程特定数据键
#include
int pthread_key_delete(pthread_key_t key); 调用成功完成则返回零,其它值表示出错
使用该函数可以销毁指定的键。由于键已经无效,因此将释放与该键关联的所有内存。引用无效键将返回错误。
如果已删除键,则使用调用 pthread_setspecific() 或 pthread_getspecific() 引用该键时,生成的结果将是不确定的。
程序员在调用删除函数之前必须释放所有线程特定资源。删除函数不会调用任何析构函数。对于每个所需的键,应当只调用 pthread_key_create() 一次。
3)设置线程特定数据
#include
int pthread_setspecific(pthread_key_t key, const void *value); 调用成功完成则返回零,其它值表示出错
该函数用于为指定的线程特定数据键设置线程特定绑定。
由于线程特定数据是类似于全局数据的,它在线程生存期内一直存在(除非显式删除了它),因而value必须是具有线程生命期的某种类型的数据(比如动态分配或者静态变量)
设置新绑定时,pthread_setspecific并不会释放原有绑定所使用的内存空间,因而程序必须自己完成这个动作。
4)获取线程特定数据
#include
void *pthread_getspecific(pthread_key_t key);
该函数用于获取调用线程的键绑定,并将该绑定存储在value 指向的位置中。
5.获取线程标识符
#include
pthread_t pthread_self(void); 返回调用线程的 thread identifier
6.比较线程 ID
#include
int pthread_equal(pthread_t tid1, pthread_t tid2);
如果 tid1 和 tid2 相等,pthread_equal() 将返回非零值,否则将返回零。如果 tid1 或 tid2 是无效的线程标识号,则结果无法预测。
7.停止执行线程
#include
int sched_yield(void); 调用成功完成则返回零,否则,返回 -1,并设置 errno 以指示错误状态。
使用sched_yield可以使当前线程停止执行,以便执行另一个具有相同或更高优先级的线程。
8.设置/获取线程的优先级
#include
int pthread_setschedparam(pthread_t tid, int policy, const struct sched_param *param); 调用成功完成则返回零,其它值表示出错
该函数用于修改现有线程的优先级。此函数对于调度策略不起作用。
int pthread_getschedparam(pthread_t tid, int policy, struct schedparam *param); 调用成功完成则返回零,其它值表示出错
该函数可用来获取现有线程的优先级。
9.信号
#include
#include
int pthread_kill(thread_t tid, int sig); 调用成功完成则返回零,其它值表示出错
该函数将信号sig发送到由 tid 指定的线程。tid 所指定的线程必须与调用线程在同一个进程中。sig 参数必须是合法的信号。如果sig为零,将执行错误检查,但并不实际发送信号。此错误检查可用来检查 tid 的有效性。
int pthread_sigmask(int how, const sigset_t *new, sigset_t *old);调用成功完成则返回零,其它值表示出错
该函数用于更改或检查调用线程的信号掩码。参数意义类似于sigprocmask。
10.终止线程
#include
void pthread_exit(void *status);
该函数用于终止线程。同时会释放所有线程特定数据绑定。如果调用线程尚未分离,则线程ID和status指定的退出状态将保持不变,直到应用程序调用pthread_join() 以等待该线程。否则,将忽略status。线程ID可以立即回收。
调用线程将终止,退出状态设置为 status 的内容。
线程可通过以下方法来终止执行:
- 从线程的第一个(最外面的)过程返回,即线程的“main”函数返回
- 调用 pthread_exit(),提供退出状态
- 使用 POSIX 取消函数执行终止操作
线程的缺省行为是延迟终止(实际上类似于进程退出,进程退出时,内核会维护退出进程的终止状态),直到其他线程通过"joining" 延迟的线程确认其已死亡。join 的结果是joining 线程得到已终止线程的退出状态,已终止的线程将消失。
如果初始线程(即调用main的线程)从 main调用返回或调用了exit,则整个进程及其所有的线程将终止。因此,一定要确保初始线程不会从main过早地返回。而如果主线程仅仅调用了pthread_exit,则仅主线程本身终止。进程及进程内的其他线程将继续存在。所有线程都终止时,进程才终止。
11.取消线程
取消操作允许线程请求终止其所在进程中的任何其他线程。典型的情况如:
- 用户请求关闭或退出正在运行的应用程序。
- 多个线程在做同一个任务,其中的某个线程可能最终完成了该任务,而其他线程还在继续运行。由于正在运行的线程此时没有任何用处,因此应当取消这些线程。
1).取消点
仅当取消操作安全时才能取消线程。pthreads 标准指定了几个取消点,其中包括:
- 通过 pthread_testcancel 调用以编程方式建立线程取消点。
- 线程等待 pthread_cond_wait 或 pthread_cond_timedwait(3C) 中的特定条件出现。
- 被 sigwait(2) 阻塞的线程。
- 一些标准的库调用。通常,这些调用包括线程可基于其阻塞的函数。
缺省情况下取消功能是启用的。如果禁用取消功能,则会导致延迟所有的取消请求,直到再次启用取消请求。
2).放置取消点
执行取消操作存在一定的危险。大多数危险都与完全恢复不变量和释放共享资源有关。取消线程时一定要格外小心,否则可能会使互斥保留为锁定状态,从而导致死锁。或者,已取消的线程可能保留已分配的内存区域,但是系统无法识别这一部分内存,从而无法释放它。
标准C库指定了一个取消接口用于以编程方式允许或禁止取消功能。该库定义的取消点是一组可能会执行取消操作的点。该库还允许定义取消处理程序的范围,以确保这些处理程序在预期的时间和位置运行。取消处理程序提供的清理服务可以将资源和状态恢复到与起点一致的状态。
由于取消操作存在一定的危险型,因而应做到:
- 在充分了解程序的基础上去设置取消点和执行取消处理程序。互斥的地方肯定不是取消点。
- 取消区域应该被限制在没有外部依赖性的代码段中,因为外部依赖性可能会产生挂起的资源或未解决的状态条件。
3).取消线程
#include
int pthread_cancel(pthread_t thread); 调用成功完成则返回零,其它值表示出错
取消请求的处理方式取决于目标线程的状态。状态由以下两个函数确定:pthread_setcancelstate和pthread_setcanceltype。
4).启用或禁用取消功能
#include
int pthread_setcancelstate(int state, int *oldstate); 调用成功完成则返回零,其它值表示出错
该函数用于启用或禁用线程取消功能。创建线程时,缺省情况下线程取消功能处于启用状态。
5).设置取消类型
#include
int pthread_setcanceltype(int type, int *oldtype); 调用成功完成则返回零,其它值表示出错
该函数可以将取消类型设置为延迟或异步模式。
创建线程时,缺省情况下会将取消类型设置为延迟模式。在延迟模式下,只能在取消点取消线程。在异步模式下,可以在执行过程中的任意一点取消线程。建议不使用异步模式,因为并不是所有代码位置都可以安全的取消线程。
6).创建取消点
#include
void pthread_testcancel(void);
该函数用于为线程建立取消点。
当线程取消功能处于启用状态且取消类型设置为延迟模式时,pthread_testcancel() 函数有效。如果在取消功能处于禁用状态下调用 pthread_testcancel(),则该函数不起作用。必须保证仅在线程取消操作安全的代码段中插入 pthread_testcancel()。
7).清理处理程序
使用清理处理程序,可以将状态恢复到与起点一致的状态,其中包括清理已分配的资源和恢复不变量。
a)pthread_cleanup_push
#include
void pthread_cleanup_push(void(*routine)(void *), void *args);
该函数用于将清理处理程序推送到清理栈。
b)pthread_cleanup_pop
#include
void pthread_cleanup_pop(int execute);
该函数用于从栈中弹出清理处理程序。如果弹出函数中的参数为非零值,则会从栈中删除该处理程序并执行该处理程序。如果该参数为零,则会弹出该处理程序,而不执行它。线程显式或隐式调用pthread_exit时,或线程接受取消请求时,会使用非零参数有效地调用pthread_cleanup_pop()。