本章介绍 POSIX 线程的基本线程编程例程。本章介绍缺省线程(即,具有缺省属性值的线程),这是多线程编程中最常用的线程。本章还介绍如何创建和使用具有非缺省属性的线程。
本章介绍的 POSIX 例程具有与最初的 Solaris 多线程库相似的编程接口。
下面简要论述了特定任务及其相关手册页。
如果未指定属性对象,则该对象为 NULL,系统会创建具有以下属性的缺省线程:
进程范围
非分离
缺省栈和缺省栈大小
零优先级
还可以用 pthread_attr_init() 创建缺省属性对象,然后使用该属性对象来创建缺省线程。有关详细信息,请参见初始化属性一节。
使用 pthread_create(3C) 可以向当前进程中添加新的受控线程。
int pthread_create(pthread_t *tid, const pthread_attr_t *tattr, void*(*start_routine)(void *), void *arg);
#includepthread_attr_t() tattr; pthread_t tid; extern void *start_routine(void *arg); void *arg; int ret; /* default behavior*/ ret = pthread_create(&tid, NULL, start_routine, arg); /* initialized with default attributes */ ret = pthread_attr_init(&tattr); /* default behavior specified*/ ret = pthread_create(&tid, &tattr, start_routine, arg);
使用具有必要状态行为的 attr 调用 pthread_create() 函数。 start_routine 是新线程最先执行的函数。当 start_routine 返回时,该线程将退出,其退出状态设置为由 start_routine 返回的值。请参见pthread_create 语法。
当 pthread_create() 成功时,所创建线程的 ID 被存储在由 tid 指向的位置中。
使用 NULL 属性参数或缺省属性调用 pthread_create() 时,pthread_create() 会创建一个缺省线程。在对 tattr 进行初始化之后,该线程将获得缺省行为。
pthread_create() 在调用成功完成之后返回零。其他任何返回值都表示出现了错误。如果检测到以下任一情况,pthread_create() 将失败并返回相应的值。
EAGAIN
描述:超出了系统限制,如创建的线程太多。
EINVAL
描述:tattr 的值无效。
pthread_join() 函数会一直阻塞调用线程,直到指定的线程终止。
使用 pthread_join(3C) 等待线程终止。
int pthread_join(thread_t tid, void **status);
#includepthread_t tid; int ret; void *status; /* waiting to join thread "tid" with status */ ret = pthread_join(tid, &status); /* waiting to join thread "tid" without status */ ret = pthread_join(tid, NULL);
指定的线程必须位于当前的进程中,而且不得是分离线程。有关线程分离的信息,请参见设置分离状态。
当 status 不是 NULL 时,status 指向某个位置,在 pthread_join() 成功返回时,将该位置设置为已终止线程的退出状态。
如果多个线程等待同一个线程终止,则所有等待线程将一直等到目标线程终止。然后,一个等待线程成功返回。其余的等待线程将失败并返回 ESRCH 错误。
在 pthread_join() 返回之后,应用程序可回收与已终止线程关联的任何数据存储空间。
调用成功完成后,pthread_join() 将返回零。其他任何返回值都表示出现了错误。如果检测到以下任一情况,pthread_join() 将失败并返回相应的值。
ESRCH
描述:没有找到与给定的线程 ID 相对应的线程。
EDEADLK
描述:将出现死锁,如一个线程等待其本身,或者线程 A 和线程 B 互相等待。
EINVAL
描述:与给定的线程 ID 相对应的线程是分离线程。
pthread_join() 仅适用于非分离的目标线程。如果没有必要等待特定线程终止之后才进行其他处理,则应当将该线程分离。
在示例 2–1 中,一个线程执行位于顶部的过程,该过程首先创建一个辅助线程来执行 fetch() 过程。fetch() 执行复杂的数据库查找操作,查找过程需要花费一些时间。
主线程将等待查找结果,但同时还执行其他操作。因此,主线程将执行其他活动,然后通过执行 pthread_join() 等待辅助线程。
将新线程的 pbe 参数作为栈参数进行传递。这个线程参数之所以能够作为栈参数传递,是因为主线程会等待辅助线程终止。不过,首选方法是使用 malloc 从堆分配存储,而不是传递指向线程栈存储的地址。如果将该参数作为地址传递到线程栈存储,则该地址可能无效或者在线程终止时会被重新分配。
void mainline (...) { struct phonebookentry *pbe; pthread_attr_t tattr; pthread_t helper; void *status; pthread_create(&helper, NULL, fetch, &pbe); /* do something else for a while */ pthread_join(helper, &status); /* it's now safe to use result */ } void *fetch(struct phonebookentry *arg) { struct phonebookentry *npbe; /* fetch value from a database */ npbe = search (prog_name) if (npbe != NULL) *arg = *npbe; pthread_exit(0); } struct phonebookentry { char name[64]; char phonenumber[32]; char flags[16]; }
pthread_detach(3C) 是 pthread_join(3C) 的替代函数,可回收创建时 detachstate 属性设置为 PTHREAD_CREATE_JOINABLE 的线程的存储空间。
int pthread_detach(thread_t tid);
#includepthread_t tid; int ret; /* detach thread tid */ ret = pthread_detach(tid);
pthread_detach() 函数用于指示应用程序在线程 tid 终止时回收其存储空间。如果 tid 尚未终止,pthread_detach() 不会终止该线程。
pthread_detach() 在调用成功完成之后返回零。其他任何返回值都表示出现了错误。如果检测到以下任一情况,pthread_detach() 将失败并返回相应的值。
EINVAL
描述:tid 是分离线程。
ESRCH
描述:tid 不是当前进程中有效的未分离的线程。
单线程 C 程序有两类基本数据:局部数据和全局数据。对于多线程 C 程序,添加了第三类数据:线程特定数据。线程特定数据与全局数据非常相似,区别在于前者为线程专有。
线程特定数据基于每线程进行维护。TSD(特定于线程的数据)是定义和引用线程专用数据的唯一方法。每个线程特定数据项都与一个作用于进程内所有线程的键关联。通过使用 key,线程可以访问基于每线程进行维护的指针 (void *)。
int pthread_key_create(pthread_key_t *key, void (*destructor) (void *));
#includepthread_key_t key; int ret; /* key create without destructor */ ret = pthread_key_create(&key, NULL); /* key create with destructor */ ret = pthread_key_create(&key, destructor);
可以使用 pthread_key_create(3C) 分配用于标识进程中线程特定数据的键。键对进程中的所有线程来说是全局的。创建线程特定数据时,所有线程最初都具有与该键关联的 NULL 值。
使用各个键之前,会针对其调用一次 pthread_key_create()。不存在对键(为进程中所有的线程所共享)的隐含同步。
创建键之后,每个线程都会将一个值绑定到该键。这些值特定于线程并且针对每个线程单独维护。如果创建该键时指定了 destructor 函数,则该线程终止时,系统会解除针对每线程的绑定。
当 pthread_key_create() 成功返回时,会将已分配的键存储在 key 指向的位置中。调用方必须确保对该键的存储和访问进行正确的同步。
使用可选的析构函数 destructor 可以释放过时的存储。如果某个键具有非 NULL destructor 函数,而线程具有一个与该键关联的非 NULL 值,则该线程退出时,系统将使用当前的相关值调用 destructor 函数。destructor 函数的调用顺序不确定。
pthread_key_create() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,pthread_key_create() 将失败并返回相应的值。
EAGAIN
描述:key 名称空间已经用完。
ENOMEM
描述:此进程中虚拟内存不足,无法创建新键。
使用 pthread_key_delete(3C) 可以销毁现有线程特定数据键。由于键已经无效,因此将释放与该键关联的所有内存。引用无效键将返回错误。Solaris 线程中没有类似的函数。
int pthread_key_delete(pthread_key_t key);
#includepthread_key_t key; int ret; /* key previously created */ ret = pthread_key_delete(key);
如果已删除键,则使用调用 pthread_setspecific() 或 pthread_getspecific() 引用该键时,生成的结果将是不确定的。
程序员在调用删除函数之前必须释放所有线程特定资源。删除函数不会调用任何析构函数。反复调用 pthread_key_create() 和 pthread_key_delete() 可能会产生问题。如果 pthread_key_delete() 将键标记为无效,而之后 key 的值不再被重用,那么反复调用它们就会出现问题。对于每个所需的键,应当只调用 pthread_key_create() 一次。
pthread_key_delete() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,pthread_key_delete() 将失败并返回相应的值。
EINVAL
描述:key 的值无效。
使用 pthread_setspecific(3C) 可以为指定线程特定数据键设置线程特定绑定。
int pthread_setspecific(pthread_key_t key, const void *value);
#includepthread_key_t key; void *value; int ret; /* key previously created */ ret = pthread_setspecific(key, value);
pthread_setspecific() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,pthread_setspecific() 将失败并返回相应的值。
ENOMEM
描述:虚拟内存不足。
EINVAL
描述:key 无效。
注 –
设置新绑定时,pthread_setspecific() 不会释放其存储空间。必须释放现有绑定,否则会出现内存泄漏。
请使用 pthread_getspecific(3C) 获取调用线程的键绑定,并将该绑定存储在 value 指向的位置中。
void *pthread_getspecific(pthread_key_t key);
#includepthread_key_t key; void *value; /* key previously created */ value = pthread_getspecific(key);
pthread_getspecific 不返回任何错误。
示例 2–2 显示的代码是从多线程程序中摘录出来的。这段代码可以由任意数量的线程执行,但该代码引用了两个全局变量:errno 和 mywindow。这些全局值实际上应当是对每个线程专用项的引用。
body() { ... while (write(fd, buffer, size) == -1) { if (errno != EINTR) { fprintf(mywindow, "%s/n", strerror(errno)); exit(1); } } ... }
errno 引用应该从线程所调用的例程获取系统错误,而从其他线程所调用的例程获取系统错误。因此,线程不同,引用 errno 所指向的存储位置也不同。
mywindow 变量指向一个 stdio (标准 IO)流,作为线程专属的流窗口。因此,与 errno 一样,线程不同,引用 mywindow 所指向的存储位置也不同。最终,这个引用指向不同的流窗口。唯一的区别在于系统负责处理 errno,而程序员必须处理对 mywindow 的引用。
下一个示例说明对 mywindow 的引用如何工作。预处理程序会将对 mywindow 的引用转换为对 _mywindow() 过程的调用。
此例程随后调用 pthread_getspecific()。pthread_getspecific() 接收 mywindow_key 全局变量作为输入参数,以输出参数 win 返回该线程的窗口。
thread_key_t mywin_key; FILE *_mywindow(void) { FILE *win; win = pthread_getspecific(mywin_key); return(win); } #define mywindow _mywindow() void routine_uses_win( FILE *win) { ... } void thread_start(...) { ... make_mywin(); ... routine_uses_win( mywindow ) ... }
mywin_key 变量标识一类变量,对于该类变量,每个线程都有其各自的专用副本。这些变量是线程特定数据。每个线程都调用 make_mywin() 以初始化其时限并安排其 mywindow 实例以引用线程特定数据。
调用此例程之后,此线程可以安全地引用 mywindow,调用 _mywindow() 之后,此线程将获得对其专用时限的引用。引用 mywindow 类似于直接引用线程专用数据。
示例 2–4 说明如何设置引用。
void make_mywindow(void) { FILE **win; static pthread_once_t mykeycreated = PTHREAD_ONCE_INIT; pthread_once(&mykeycreated, mykeycreate); win = malloc(sizeof(*win)); create_window(win, ...); pthread_setspecific(mywindow_key, win); } void mykeycreate(void) { pthread_key_create(&mywindow_key, free_key); } void free_key(void *win) { free(win); }
首先,得到一个唯一的键值,mywin_key。此键用于标识线程特定数据类。第一个调用 make_mywin() 的线程最终会调用 pthread_key_create(),该函数将唯一的 key 赋给其第一个参数。第二个参数是 destructor 函数,用于在线程终止后将该线程的特定于该线程的数据项实例解除分配。
接下来为调用方的线程特定数据项的实例分配存储空间。获取已分配的存储空间,调用 create_window(),以便为该线程设置时限。win 指向为该时限分配的存储空间。最后,调用 pthread_setspecific(),将 win 与该键关联。
以后,每当线程调用 pthread_getspecific() 以传递全局 key,线程都会获得它在前一次调用 pthread_setspecific() 时设置的与该键关联的值)。
线程终止时,会调用在 pthread_key_create() 中设置的 destructor 函数。每个 destructor 函数仅在终止线程通过调用 pthread_setspecific() 为 key 赋值之后才会被调用。
请使用 pthread_self(3C) 获取调用线程的 thread identifier。
pthread_t pthread_self(void);
#includepthread_t tid; tid = pthread_self();
pthread_self() 返回调用线程的 thread identifier。
请使用 pthread_equal(3C) 对两个线程的线程标识号进行比较。
int pthread_equal(pthread_t tid1, pthread_t tid2);
#includepthread_t tid1, tid2; int ret; ret = pthread_equal(tid1, tid2);
如果 tid1 和 tid2 相等,pthread_equal() 将返回非零值,否则将返回零。如果 tid1 或 tid2 是无效的线程标识号,则结果无法预测。
使用 pthread_once(3C),可以在首次调用 pthread_once 时调用初始化例程。以后调用 pthread_once() 将不起作用。
int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
#includepthread_once_t once_control = PTHREAD_ONCE_INIT; int ret; ret = pthread_once(&once_control, init_routine);
once_control 参数用来确定是否已调用相关的初始化例程。
pthread_once() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,pthread_once() 将失败并返回相应的值。
EINVAL
描述:once_control 或 init_routine 是 NULL。
使用 sched_yield(3RT),可以使当前线程停止执行,以便执行另一个具有相同或更高优先级的线程。
int sched_yield(void);
#includeint ret; ret = sched_yield();
sched_yield() 在成功完成之后返回零。否则,返回 -1,并设置 errno 以指示错误状态。
ENOSYS
描述:本实现不支持 sched_yield。
请使用 pthread_setschedparam(3C) 修改现有线程的优先级。此函数对于调度策略不起作用。
int pthread_setschedparam(pthread_t tid, int policy, const struct sched_param *param);
#includepthread_t tid; int ret; struct sched_param param; int priority; /* sched_priority will be the priority of the thread */ sched_param.sched_priority = priority; policy = SCHED_OTHER; /* scheduling parameters of target thread */ ret = pthread_setschedparam(tid, policy, ¶m);
pthread_setschedparam() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,pthread_setschedparam() 函数将失败并返回相应的值。
EINVAL
描述:所设置属性的值无效。
ENOTSUP
描述:尝试将该属性设置为不受支持的值。
pthread_getschedparam(3C) 可用来获取现有线程的优先级。
int pthread_getschedparam(pthread_t tid, int policy, struct schedparam *param);
#includepthread_t tid; sched_param param; int priority; int policy; int ret; /* scheduling parameters of target thread */ ret = pthread_getschedparam (tid, &policy, ¶m); /* sched_priority contains the priority of the thread */ priority = param.sched_priority;
pthread_getschedparam() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
ESRCH
描述:tid 指定的值不引用现有的线程。
请使用 pthread_kill(3C) 向线程发送信号。
int pthread_kill(thread_t tid, int sig);
#include#include int sig; pthread_t tid; int ret; ret = pthread_kill(tid, sig);
pthread_kill() 将信号 sig 发送到由 tid 指定的线程。tid 所指定的线程必须与调用线程在同一个进程中。sig 参数必须来自 signal(5) 提供的列表。
如果 sig 为零,将执行错误检查,但并不实际发送信号。此错误检查可用来检查 tid 的有效性。
pthread_kill() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,pthread_kill() 将失败并返回相应的值。
EINVAL
描述:sig 是无效的信号量。
ESRCH
描述:当前的进程中找不到 tid。
请使用 pthread_sigmask(3C) 更改或检查调用线程的信号掩码。
int pthread_sigmask(int how, const sigset_t *new, sigset_t *old);
#include#include int ret; sigset_t old, new; ret = pthread_sigmask(SIG_SETMASK, &new, &old); /* set new mask */ ret = pthread_sigmask(SIG_BLOCK, &new, &old); /* blocking mask */ ret = pthread_sigmask(SIG_UNBLOCK, &new, &old); /* unblocking */
how 用来确定如何更改信号组。how 可以为以下值之一:
SIG_BLOCK。向当前的信号掩码中添加 new,其中 new 表示要阻塞的信号组。
SIG_UNBLOCK。从当前的信号掩码中删除 new,其中 new 表示要取消阻塞的信号组。
SIG_SETMASK。将当前的信号掩码替换为 new,其中 new 表示新的信号掩码。
当 new 的值为 NULL 时,how 的值没有意义,线程的信号掩码不发生变化。要查询当前已阻塞的信号,请将 NULL 值赋给 new 参数。
除非 old 变量为 NULL,否则 old 指向用来存储以前的信号掩码的空间。
pthread_sigmask() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,pthread_sigmask() 将失败并返回相应的值。
EINVAL
描述:未定义 how 的值。
请参见解决方案: pthread_atfork中有关 pthread_atfork(3C) 的论述。
int pthread_atfork(void (*prepare) (void), void (*parent) (void), void (*child) (void) );
pthread_atfork() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,pthread_atfork() 将失败并返回相应的值。
ENOMEM
描述:表空间不足,无法记录 Fork 处理程序地址。
请使用 pthread_exit(3C) 终止线程。
void pthread_exit(void *status);
#includevoid *status; pthread_exit(status); /* exit with status */
pthread_exit() 函数可用来终止调用线程。将释放所有线程特定数据绑定。如果调用线程尚未分离,则线程 ID 和 status 指定的退出状态将保持不变,直到应用程序调用 pthread_join() 以等待该线程。否则,将忽略 status。线程 ID 可以立即回收。有关线程分离的信息,请参见设置分离状态。
调用线程将终止,退出状态设置为 status 的内容。
线程可通过以下方法来终止执行:
从线程的第一个(最外面的)过程返回,使线程启动例程。请参见 pthread_create。
调用 pthread_exit(),提供退出状态。
使用 POSIX 取消函数执行终止操作。请参见 pthread_cancel()。
线程的缺省行为是拖延,直到其他线程通过 "joining" 拖延线程确认其已死亡。此行为与非分离的缺省 pthread_create() 属性相同,请参见 pthread_detach。join 的结果是 joining 线程得到已终止线程的退出状态,已终止的线程将消失。
有一个重要的特殊情况,即当初始线程(即调用 main() 的线程)从 main() 调用返回时或调用 exit() 时,整个进程及其所有的线程将终止。因此,一定要确保初始线程不会从 main() 过早地返回。
请注意,如果主线程仅仅调用了 pthread_exit,则仅主线程本身终止。进程及进程内的其他线程将继续存在。所有线程都已终止时,进程也将终止。
取消操作允许线程请求终止其所在进程中的任何其他线程。不希望或不需要对一组相关的线程执行进一步操作时,可以选择执行取消操作。
取消线程的一个示例是异步生成取消条件,例如,用户请求关闭或退出正在运行的应用程序。另一个示例是完成由许多线程执行的任务。其中的某个线程可能最终完成了该任务,而其他线程还在继续运行。由于正在运行的线程此时没有任何用处,因此应当取消这些线程。
仅当取消操作安全时才应取消线程。pthreads 标准指定了几个取消点,其中包括:
通过 pthread_testcancel 调用以编程方式建立线程取消点。
线程等待 pthread_cond_wait 或 pthread_cond_timedwait(3C) 中的特定条件出现。
被 sigwait(2) 阻塞的线程。
一些标准的库调用。通常,这些调用包括线程可基于其阻塞的函数。有关列表,请参见 cancellation(5) 手册页。
缺省情况下将启用取消功能。有时,您可能希望应用程序禁用取消功能。如果禁用取消功能,则会导致延迟所有的取消请求,直到再次启用取消请求。
有关禁用取消功能的信息,请参见pthread_setcancelstate 语法。
执行取消操作存在一定的危险。大多数危险都与完全恢复不变量和释放共享资源有关。取消线程时一定要格外小心,否则可能会使互斥保留为锁定状态,从而导致死锁。或者,已取消的线程可能保留已分配的内存区域,但是系统无法识别这一部分内存,从而无法释放它。
标准 C 库指定了一个取消接口用于以编程方式允许或禁止取消功能。该库定义的取消点是一组可能会执行取消操作的点。该库还允许定义取消处理程序的范围,以确保这些处理程序在预期的时间和位置运行。取消处理程序提供的清理服务可以将资源和状态恢复到与起点一致的状态。
必须对应用程序有一定的了解,才能放置取消点并执行取消处理程序。互斥肯定不是取消点,只应当在必要时使之保留尽可能短的时间。
请将异步取消区域限制在没有外部依赖性的序列,因为外部依赖性可能会产生挂起的资源或未解决的状态条件。在从某个备用的嵌套取消状态返回时,一定要小心地恢复取消状态。该接口提供便于进行恢复的功能:pthread_setcancelstate(3C) 在所引用的变量中保留当前的取消状态,pthread_setcanceltype(3C) 以同样的方式保留当前的取消类型。
在以下三种不同的情况下可能会执行取消操作:
异步
执行序列中按标准定义的各个点
调用 pthread_testcancel() 时
缺省情况下,仅在 POSIX 标准可靠定义的点执行取消操作。
无论何时,都应注意资源和状态恢已复到与起点一致的状态。
请使用 pthread_cancel(3C) 取消线程。
int pthread_cancel(pthread_t thread);
#includepthread_t thread; int ret; ret = pthread_cancel(thread);
取消请求的处理方式取决于目标线程的状态。状态由以下两个函数确定:pthread_setcancelstate(3C) 和 pthread_setcanceltype(3C)。
pthread_cancel() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
ESRCH
描述:没有找到与给定线程 ID 相对应的线程。
请使用 pthread_setcancelstate(3C) 启用或禁用线程取消功能。创建线程时,缺省情况下线程取消功能处于启用状态。
int pthread_setcancelstate(int state, int *oldstate);
#includeint oldstate; int ret; /* enabled */ ret = pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate); /* disabled */ ret = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
pthread_setcancelstate() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,pthread_setcancelstate() 函数将失败并返回相应的值。
EINVAL
描述:状态不是 PTHREAD_CANCEL_ENABLE 或 PTHREAD_CANCEL_DISABLE。
使用 pthread_setcanceltype(3C) 可以将取消类型设置为延迟或异步模式。
int pthread_setcanceltype(int type, int *oldtype);
#includeint oldtype; int ret; /* deferred mode */ ret = pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &oldtype); /* async mode*/ ret = pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);
创建线程时,缺省情况下会将取消类型设置为延迟模式。在延迟模式下,只能在取消点取消线程。在异步模式下,可以在执行过程中的任意一点取消线程。因此建议不使用异步模式。
pthread_setcanceltype() 在成功完成之后返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:类型不是 PTHREAD_CANCEL_DEFERRED 或 PTHREAD_CANCEL_ASYNCHRONOUS。
请使用 pthread_testcancel(3C) 为线程建立取消点。
void pthread_testcancel(void);
#includepthread_testcancel();
当线程取消功能处于启用状态且取消类型设置为延迟模式时,pthread_testcancel() 函数有效。如果在取消功能处于禁用状态下调用 pthread_testcancel(),则该函数不起作用。
请务必仅在线程取消操作安全的序列中插入 pthread_testcancel()。除通过 pthread_testcancel() 调用以编程方式建立的取消点以外,pthread 标准还指定了几个取消点。有关更多详细信息,请参见取消点。
pthread_testcancel() 没有返回值。
使用清理处理程序,可以将状态恢复到与起点一致的状态,其中包括清理已分配的资源和恢复不变量。使用 pthread_cleanup_push(3C) 和 pthread_cleanup_pop(3C) 函数可以管理清理处理程序。
在程序的同一词法域中可以推送和弹出清理处理程序。推送和弹出操作应当始终匹配,否则会生成编译器错误。
请使用 pthread_cleanup_push(3C) 将清理处理程序推送到清理栈 (LIFO)。
void pthread_cleanup_push(void(*routine)(void *), void *args);
#include/* push the handler "routine" on cleanup stack */ pthread_cleanup_push (routine, arg);
pthread_cleanup_push() 没有返回值。
请使用 pthread_cleanup_pop(3C) 从清理栈中弹出清理处理程序。
void pthread_cleanup_pop(int execute);
#include/* pop the "func" out of cleanup stack and execute "func" */ pthread_cleanup_pop (1); /* pop the "func" and DONT execute "func" */ pthread_cleanup_pop (0);
如果弹出函数中的参数为非零值,则会从栈中删除该处理程序并执行该处理程序。如果该参数为零,则会弹出该处理程序,而不执行它。
线程显式或隐式调用 pthread_exit(3C) 时,或线程接受取消请求时,会使用非零参数有效地调用 pthread_cleanup_pop()。
pthread_cleanup_pop() 没有返回值。
本章介绍了可用于线程的同步类型,还说明了使用同步的时间和方法。
互斥锁属性
使用互斥锁
条件变量属性
使用条件变量
使用信号进行同步
读写锁属性
设置互斥锁属性的协议
跨进程边界同步
比较元语
同步对象是内存中的变量,可以按照与访问数据完全相同的方式对其进行访问。不同进程中的线程可以通过放在由线程控制的共享内存中的同步对象互相通信。尽管不同进程中的线程通常互不可见,但这些线程仍可以互相通信。
同步对象还可以放在文件中。同步对象可以比创建它的进程具有更长的生命周期。
同步对象具有以下可用类型:
互斥锁
条件变量
读写锁
信号
同步的作用包括以下方面:
同步是确保共享数据一致性的唯一方法。
两个或多个进程中的线程可以合用一个同步对象。由于重新初始化同步对象会将对象的状态设置为解除锁定,因此应仅由其中的一个协作进程来初始化同步对象。
同步可确保可变数据的安全性。
进程可以映射文件并指示该进程中的线程获取记录锁。一旦获取了记录锁,映射此文件的任何进程中尝试获取该锁的任何线程都会被阻塞,直到释放该锁为止。
访问一个基本类型变量(如整数)时,可以针对一个内存负荷使用多个存储周期。如果整数没有与总线数据宽度对齐或者大于数据宽度,则会使用多个存储周期。尽管这种整数不对齐现象不会出现在 SPARC® Platform Edition 体系结构上,但是可移植的程序却可能会出现对齐问题。
注 –
在 32 位体系结构上,long
long
不是原子类型。(原子操作不能划分成更小的操作。)long
long
可作为两个 32 位值进行读写。类型 int
、char
、float
和指针在 SPARC Platform Edition 计算机和 Intel 体系结构的计算机上是原子类型。
使用互斥锁(互斥)可以使线程按顺序执行。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多个线程。互斥锁还可以保护单线程代码。
要更改缺省的互斥锁属性,可以对属性对象进行声明和初始化。通常,互斥锁属性会设置在应用程序开头的某个位置,以便可以快速查找和轻松修改。表 4–1 列出了用来处理互斥锁属性的函数。
表 4–1 互斥锁属性例程
操作 |
相关函数说明 |
---|---|
初始化互斥锁属性对象 |
pthread_mutexattr_init 语法 |
销毁互斥锁属性对象 |
pthread_mutexattr_destroy 语法 |
设置互斥锁范围 |
pthread_mutexattr_setpshared 语法 |
获取互斥锁范围 |
pthread_mutexattr_getpshared 语法 |
设置互斥锁的类型属性 |
pthread_mutexattr_settype 语法 |
获取互斥锁的类型属性 |
pthread_mutexattr_gettype 语法 |
设置互斥锁属性的协议 |
pthread_mutexattr_setprotocol 语法 |
获取互斥锁属性的协议 |
pthread_mutexattr_getprotocol 语法 |
设置互斥锁属性的优先级上限 |
pthread_mutexattr_setprioceiling 语法 |
获取互斥锁属性的优先级上限 |
pthread_mutexattr_getprioceiling 语法 |
设置互斥锁的优先级上限 |
pthread_mutex_setprioceiling 语法 |
获取互斥锁的优先级上限 |
pthread_mutex_getprioceiling 语法 |
设置互斥锁的强健属性 |
pthread_mutexattr_setrobust_np 语法 |
获取互斥锁的强健属性 |
pthread_mutexattr_getrobust_np 语法 |
表 4–2 中显示了在定义互斥范围时 Solaris 线程和 POSIX 线程之间的差异。
表 4–2 互斥锁范围比较
Solaris |
POSIX |
定义 |
---|---|---|
USYNC_PROCESS |
PTHREAD_PROCESS_SHARED |
用于同步该进程和其他进程中的线程 |
USYNC_PROCESS_ROBUST |
无 POSIX 等效项 |
用于在进程间可靠地同步线程 |
USYNC_THREAD |
PTHREAD_PROCESS_PRIVATE |
用于仅同步该进程中的线程 |
使用 pthread_mutexattr_init(3C) 可以将与互斥锁对象相关联的属性初始化为其缺省值。在执行过程中,线程系统会为每个属性对象分配存储空间。
int pthread_mutexattr_init(pthread_mutexattr_t *mattr);
#includepthread_mutexattr_t mattr; int ret; /* initialize an attribute to default value */ ret = pthread_mutexattr_init(&mattr);
调用此函数时,pshared 属性的缺省值为 PTHREAD_PROCESS_PRIVATE。该值表示可以在进程内使用经过初始化的互斥锁。
mattr 的类型为 opaque
,其中包含一个由系统分配的属性对象。mattr 范围可能的值为 PTHREAD_PROCESS_PRIVATE 和 PTHREAD_PROCESS_SHARED。PTHREAD_PROCESS_PRIVATE 是缺省值。
对于互斥锁属性对象,必须首先通过调用 pthread_mutexattr_destroy(3C) 将其销毁,才能重新初始化该对象。pthread_mutexattr_init() 调用会导致分配类型为 opaque 的对象。如果未销毁该对象,则会导致内存泄漏。
pthread_mutexattr_init() 成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
ENOMEM
描述:内存不足,无法初始化互斥锁属性对象。
pthread_mutexattr_destroy(3C) 可用来取消分配用于维护 pthread_mutexattr_init() 所创建的属性对象的存储空间。
int pthread_mutexattr_destroy(pthread_mutexattr_t *mattr)
#includepthread_mutexattr_t mattr; int ret; /* destroy an attribute */ ret = pthread_mutexattr_destroy(&mattr);
pthread_mutexattr_destroy() 成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:由 mattr 指定的值无效。
pthread_mutexattr_setpshared(3C) 可用来设置互斥锁变量的作用域。
int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared);
#includepthread_mutexattr_t mattr; int ret; ret = pthread_mutexattr_init(&mattr); /* * resetting to its default value: private */ ret = pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_PRIVATE);
互斥锁变量可以是进程专用的(进程内)变量,也可以是系统范围内的(进程间)变量。要在多个进程中的线程之间共享互斥锁,可以在共享内存中创建互斥锁,并将 pshared 属性设置为 PTHREAD_PROCESS_SHARED。 此行为与最初的 Solaris 线程实现中 mutex_init() 中的 USYNC_PROCESS 标志等效。
如果互斥锁的 pshared 属性设置为 PTHREAD_PROCESS_PRIVATE,则仅有那些由同一个进程创建的线程才能够处理该互斥锁。
pthread_mutexattr_setpshared() 成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:由 mattr 指定的值无效。
pthread_mutexattr_getpshared(3C) 可用来返回由 pthread_mutexattr_setpshared() 定义的互斥锁变量的范围。
int pthread_mutexattr_getpshared(pthread_mutexattr_t *mattr, int *pshared);
#includepthread_mutexattr_t mattr; int pshared, ret; /* get pshared of mutex */ ret = pthread_mutexattr_getpshared(&mattr, &pshared);
此函数可为属性对象 mattr 获取 pshared 的当前值。该值为 PTHREAD_PROCESS_SHARED 或 PTHREAD_PROCESS_PRIVATE。
pthread_mutexattr_getpshared() 成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:由 mattr 指定的值无效。
pthread_mutexattr_settype(3C) 可用来设置互斥锁的 type 属性。
#includeint pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type);
类型属性的缺省值为 PTHREAD_MUTEX_DEFAULT。
type 参数指定互斥锁的类型。以下列出了有效的互斥锁类型:
PTHREAD_MUTEX_NORMAL
描述:此类型的互斥锁不会检测死锁。如果线程在不首先解除互斥锁的情况下尝试重新锁定该互斥锁,则会产生死锁。尝试解除由其他线程锁定的互斥锁会产生不确定的行为。如果尝试解除锁定的互斥锁未锁定,则会产生不确定的行为。
PTHREAD_MUTEX_ERRORCHECK
描述:此类型的互斥锁可提供错误检查。如果线程在不首先解除锁定互斥锁的情况下尝试重新锁定该互斥锁,则会返回错误。如果线程尝试解除锁定的互斥锁已经由其他线程锁定,则会返回错误。如果线程尝试解除锁定的互斥锁未锁定,则会返回错误。
PTHREAD_MUTEX_RECURSIVE
描述:如果线程在不首先解除锁定互斥锁的情况下尝试重新锁定该互斥锁,则可成功锁定该互斥锁。 与 PTHREAD_MUTEX_NORMAL 类型的互斥锁不同,对此类型互斥锁进行重新锁定时不会产生死锁情况。多次锁定互斥锁需要进行相同次数的解除锁定才可以释放该锁,然后其他线程才能获取该互斥锁。如果线程尝试解除锁定的互斥锁已经由其他线程锁定,则会返回错误。 如果线程尝试解除锁定的互斥锁未锁定,则会返回错误。
PTHREAD_MUTEX_DEFAULT
描述:如果尝试以递归方式锁定此类型的互斥锁,则会产生不确定的行为。对于不是由调用线程锁定的此类型互斥锁,如果尝试对它解除锁定,则会产生不确定的行为。对于尚未锁定的此类型互斥锁,如果尝试对它解除锁定,也会产生不确定的行为。允许在实现中将该互斥锁映射到其他互斥锁类型之一。对于 Solaris 线程,PTHREAD_PROCESS_DEFAULT 会映射到 PTHREAD_PROCESS_NORMAL。
如果运行成功,pthread_mutexattr_settype 函数会返回零。否则,将返回用于指明错误的错误号。
EINVAL
描述:值为 type 无效。
EINVAL
描述:attr 指定的值无效。
pthread_mutexattr_gettype(3C) 可用来获取由 pthread_mutexattr_settype() 设置的互斥锁的 type 属性。
#includeint pthread_mutexattr_gettype(pthread_mutexattr_t *attr , int *type);
类型属性的缺省值为 PTHREAD_MUTEX_DEFAULT。
type 参数指定互斥锁的类型。有效的互斥锁类型包括:
PTHREAD_MUTEX_NORMAL
PTHREAD_MUTEX_ERRORCHECK
PTHREAD_MUTEX_RECURSIVE
PTHREAD_MUTEX_DEFAULT
有关每种类型的说明,请参见pthread_mutexattr_settype 语法。
如果成功完成,pthread_mutexattr_gettype() 会返回 0。其他任何返回值都表示出现了错误。
pthread_mutexattr_setprotocol(3C) 可用来设置互斥锁属性对象的协议属性。
#includeint pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol);
attr 指示以前调用 pthread_mutexattr_init() 时创建的互斥锁属性对象。
protocol 可定义应用于互斥锁属性对象的协议。
pthread.h 中定义的 protocol 可以是以下值之一:PTHREAD_PRIO_NONE、PTHREAD_PRIO_INHERIT 或 PTHREAD_PRIO_PROTECT。
PTHREAD_PRIO_NONE
线程的优先级和调度不会受到互斥锁拥有权的影响。
PTHREAD_PRIO_INHERIT
此协议值(如 thrd1)会影响线程的优先级和调度。如果更高优先级的线程因 thrd1 所拥有的一个或多个互斥锁而被阻塞,而这些互斥锁是用 PTHREAD_PRIO_INHERIT 初始化的,则 thrd1 将以高于它的优先级或者所有正在等待这些互斥锁(这些互斥锁是 thrd1 指所拥有的互斥锁)的线程的最高优先级运行。
如果 thrd1 因另一个线程 (thrd3) 拥有的互斥锁而被阻塞,则相同的优先级继承效应会以递归方式传播给 thrd3。
使用 PTHREAD_PRIO_INHERIT 可以避免优先级倒置。低优先级的线程持有较高优先级线程所需的锁时,便会发生优先级倒置。只有在较低优先级的线程释放该锁之后,较高优先级的线程才能继续使用该锁。设置 PTHREAD_PRIO_INHERIT,以便按与预期的优先级相反的优先级处理每个线程。
如果为使用协议属性值 PTHREAD_PRIO_INHERIT 初始化的互斥锁定义了 _POSIX_THREAD_PRIO_INHERIT,则互斥锁的属主失败时会执行以下操作。属主失败时的行为取决于 pthread_mutexattr_setrobust_np() 的 robustness 参数的值。
解除锁定互斥锁。
互斥锁的下一个属主将获取该互斥锁,并返回错误 EOWNERDEAD。
互斥锁的下一个属主会尝试使该互斥锁所保护的状态一致。如果上一个属主失败,则状态可能会不一致。如果属主成功使状态保持一致,则可针对该互斥锁调用 pthread_mutex_init() 并解除锁定该互斥锁。
如果针对以前初始化的但尚未销毁的互斥锁调用 pthread_mutex_init(),则该互斥锁不会重新初始化。
如果属主无法使状态保持一致,请勿调用 pthread_mutex_init(),而是解除锁定该互斥锁。在这种情况下,所有等待的线程都将被唤醒。以后对 pthread_mutex_lock() 的所有调用将无法获取互斥锁,并将返回错误代码 ENOTRECOVERABLE。现在,通过调用 pthread_mutex_destroy() 来取消初始化该互斥锁,即可使其状态保持一致。调用 pthread_mutex_init() 可重新初始化互斥锁。
如果已获取该锁的线程失败并返回 EOWNERDEAD,则下一个属主将获取该锁及错误代码 EOWNERDEAD。
PTHREAD_PRIO_PROTECT
当线程拥有一个或多个使用 PTHREAD_PRIO_PROTECT 初始化的互斥锁时,此协议值会影响其他线程(如 thrd2)的优先级和调度。thrd2 以其较高的优先级或者以 thrd2 拥有的所有互斥锁的最高优先级上限运行。基于被 thrd2 拥有的任一互斥锁阻塞的较高优先级线程对于 thrd2 的调度没有任何影响。
如果某个线程调用 sched_setparam() 来更改初始优先级,则调度程序不会采用新优先级将该线程移到调度队列末尾。
线程拥有使用 PTHREAD_PRIO_INHERIT 或 PTHREAD_PRIO_PROTECT 初始化的互斥锁
线程解除锁定使用 PTHREAD_PRIO_INHERIT 或 PTHREAD_PRIO_PROTECT 初始化的互斥锁
一个线程可以同时拥有多个混合使用 PTHREAD_PRIO_INHERIT 和 PTHREAD_PRIO_PROTECT 初始化的互斥锁。在这种情况下,该线程将以通过其中任一协议获取的最高优先级执行。
如果成功完成,pthread_mutexattr_setprotocol() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下任一情况,pthread_mutexattr_setprotocol() 将失败并返回对应的值。
ENOSYS
描述:选项 _POSIX_THREAD_PRIO_INHERIT 和 _POSIX_THREAD_PRIO_PROTECT 均未定义并且该实现不支持此函数。
ENOTSUP
描述:protocol 指定的值不受支持。
如果出现以下任一情况,pthread_mutexattr_setprotocol() 可能会失败并返回对应的值。
EINVAL
描述:attr 或 protocol 指定的值无效。
EPERM
描述:调用方无权执行该操作。
pthread_mutexattr_getprotocol(3C) 可用来获取互斥锁属性对象的协议属性。
#includeint pthread_mutexattr_getprotocol(const pthread_mutexattr_t *attr, int *protocol);
attr 指示以前调用 pthread_mutexattr_init() 时创建的互斥锁属性对象。
protocol 包含以下协议属性之一:PTHREAD_PRIO_NONE、PTHREAD_PRIO_INHERIT 或 PTHREAD_PRIO_PROTECT。
如果成功完成,pthread_mutexattr_getprotocol() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下情况,pthread_mutexattr_getprotocol() 将失败并返回对应的值。
ENOSYS
描述:_POSIX_THREAD_PRIO_INHERIT 选项和 _POSIX_THREAD_PRIO_PROTECT 选项均未定义并且该实现不支持此函数。
如果出现以下任一情况,pthread_mutexattr_getprotocol() 可能会失败并返回对应的值。
EINVAL
描述:attr 指定的值无效。
EPERM
描述:调用方无权执行该操作。
pthread_mutexattr_setprioceiling(3C) 可用来设置互斥锁属性对象的优先级上限属性。
#includeint pthread_mutexattr_setprioceiling(pthread_mutexatt_t *attr, int prioceiling, int *oldceiling);
attr 指示以前调用 pthread_mutexattr_init() 时创建的互斥锁属性对象。
prioceiling 指定已初始化互斥锁的优先级上限。优先级上限定义执行互斥锁保护的临界段时的最低优先级。prioceiling 位于 SCHED_FIFO 所定义的优先级的最大范围内。要避免优先级倒置,请将 prioceiling 设置为高于或等于可能会锁定特定互斥锁的所有线程的最高优先级。
oldceiling 包含以前的优先级上限值。
如果成功完成,pthread_mutexattr_setprioceiling() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下任一情况,pthread_mutexattr_setprioceiling() 将失败并返回对应的值。
ENOSYS
描述:选项 _POSIX_THREAD_PRIO_PROTECT 未定义并且该实现不支持此函数。
如果出现以下任一情况,pthread_mutexattr_setprioceiling() 可能会失败并返回对应的值。
EINVAL
描述:attr 或 prioceiling 指定的值无效。
EPERM
描述:调用方无权执行该操作。
pthread_mutexattr_getprioceiling(3C) 可用来获取互斥锁属性对象的优先级上限属性。
#includeint pthread_mutexattr_getprioceiling(const pthread_mutexatt_t *attr, int *prioceiling);
attr 指定以前调用 pthread_mutexattr_init() 时创建的属性对象。
注 –
仅当定义了 _POSIX_THREAD_PRIO_PROTECT 符号时,attr 互斥锁属性对象才会包括优先级上限属性。
pthread_mutexattr_getprioceiling() 返回 prioceiling 中已初始化互斥锁的优先级上限。优先级上限定义执行互斥锁保护的临界段时的最低优先级。prioceiling 位于 SCHED_FIFO 所定义的优先级的最大范围内。要避免优先级倒置,请将 prioceiling 设置为高于或等于可能会锁定特定互斥锁的所有线程的最高优先级。
如果成功完成,pthread_mutexattr_getprioceiling() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下任一情况,pthread_mutexattr_getprioceiling() 将失败并返回对应的值。
ENOSYS
描述:_POSIX_THREAD_PRIO_PROTECT 选项未定义并且该实现不支持此函数。
如果出现以下任一情况,pthread_mutexattr_getprioceiling() 可能会失败并返回对应的值。
EINVAL
描述:attr 指定的值无效。
EPERM
描述:调用方无权执行该操作。
pthread_mutexattr_setprioceiling(3C) 可用来设置互斥锁的优先级上限。
#includeint pthread_mutex_setprioceiling(pthread_mutex_t *mutex, int prioceiling, int *old_ceiling);
pthread_mutex_setprioceiling() 可更改互斥锁 mutex 的优先级上限 prioceiling。 pthread_mutex_setprioceiling() 可锁定互斥锁(如果未锁定的话),或者一直处于阻塞状态,直到 pthread_mutex_setprioceiling() 成功锁定该互斥锁,更改该互斥锁的优先级上限并将该互斥锁释放为止。锁定互斥锁的过程无需遵循优先级保护协议。
如果 pthread_mutex_setprioceiling() 成功,则将在 old_ceiling 中返回以前的优先级上限值。如果 pthread_mutex_setprioceiling() 失败,则互斥锁的优先级上限保持不变。
如果成功完成,pthread_mutex_setprioceiling() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下情况,pthread_mutexatt_setprioceiling() 将失败并返回对应的值。
ENOSYS
描述:选项_POSIX_THREAD_PRIO_PROTECT 未定义并且该实现不支持此函数。
如果出现以下任一情况,pthread_mutex_setprioceiling() 可能会失败并返回对应的值。
EINVAL
描述:prioceiling 所请求的优先级超出了范围。
EINVAL
描述:mutex 指定的值不会引用当前存在的互斥锁。
ENOSYS
描述:该实现不支持互斥锁的优先级上限协议。
EPERM
描述:调用方无权执行该操作。
pthread_mutexattr_getprioceiling(3C) 可用来获取互斥锁的优先级上限。
#includeint pthread_mutex_getprioceiling(const pthread_mutex_t *mutex, int *prioceiling);
pthread_mutex_getprioceiling() 会返回 mutex 的优先级上限 prioceiling。
如果成功完成,pthread_mutex_getprioceiling() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下任一情况,pthread_mutexatt_getprioceiling() 将失败并返回对应的值。
ENOSYS
描述:_POSIX_THREAD_PRIO_PROTECT 选项未定义并且该实现不支持此函数。
如果出现以下任一情况,pthread_mutex_getprioceiling() 可能会失败并返回对应的值。
EINVAL
描述:mutex 指定的值不会引用当前存在的互斥锁。
ENOSYS
描述:该实现不支持互斥锁的优先级上限协议。
EPERM
描述:调用方无权执行该操作。
pthread_mutexattr_setrobust_np(3C) 可用来设置互斥锁属性对象的强健属性。
#includeint pthread_mutexattr_setrobust_np(pthread_mutexattr_t *attr, int *robustness);
注 –
仅当定义了符号 _POSIX_THREAD_PRIO_INHERIT 时,pthread_mutexattr_setrobust_np() 才适用。
attr 指示以前通过调用 pthread_mutexattr_init() 创建的互斥锁属性对象。
robustness 定义在互斥锁的属主失败时的行为。pthread.h 中定义的 robustness 的值为 PTHREAD_MUTEX_ROBUST_NP 或 PTHREAD_MUTEX_STALLED_NP。缺省值为 PTHREAD_MUTEX_STALLED_NP。
PTHREAD_MUTEX_ROBUST_NP
如果互斥锁的属主失败,则以后对 pthread_mutex_lock() 的所有调用将以不确定的方式被阻塞。
PTHREAD_MUTEX_STALLED_NP
互斥锁的属主失败时,将会解除锁定该互斥锁。互斥锁的下一个属主将获取该互斥锁,并返回错误 EOWNWERDEAD。
应用程序必须检查 pthread_mutex_lock() 的返回代码,查找返回错误 EOWNWERDEAD 的互斥锁。
互斥锁的新属主应使该互斥锁所保护的状态保持一致。如果上一个属主失败,则互斥锁状态可能会不一致。
如果新属主能够使状态保持一致,请针对该互斥锁调用 pthread_mutex_consistent_np(),并解除锁定该互斥锁。
如果新属主无法使状态保持一致,请勿针对该互斥锁调用 pthread_mutex_consistent_np(),而是解除锁定该互斥锁。
所有等待的线程都将被唤醒,以后对 pthread_mutex_lock() 的所有调用都将无法获取该互斥锁。返回代码为 ENOTRECOVERABLE。通过调用 pthread_mutex_destroy() 取消对互斥锁的初始化,并调用 pthread_mutex_int() 重新初始化该互斥锁,可使该互斥锁保持一致。
如果已获取该锁的线程失败并返回 EOWNERDEAD,则下一个属主获取该锁时将返回代码 EOWNERDEAD。
如果成功完成,pthread_mutexattr_setrobust_np() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下任一情况,pthread_mutexattr_setrobust_np() 将失败并返回对应的值。
ENOSYS
描述:选项 _POSIX_THREAD_PRIO__INHERIT 未定义,或者该实现不支持 pthread_mutexattr_setrobust_np()。
ENOTSUP
描述:robustness 指定的值不受支持。
pthread_mutexattr_setrobust_np() 可能会在出现以下情况时失败:
EINVAL
描述:attr 或 robustness 指定的值无效。
pthread_mutexattr_getrobust_np(3C) 可用来获取互斥锁属性对象的强健属性。
#includeint pthread_mutexattr_getrobust_np(const pthread_mutexattr_t *attr, int *robustness);
注 –
仅当定义了符号 _POSIX_THREAD_PRIO_INHERIT 时,pthread_mutexattr_getrobust_np() 才适用。
attr 指示以前通过调用 pthread_mutexattr_init() 创建的互斥锁属性对象。
robustness 是互斥锁属性对象的强健属性值。
如果成功完成,pthread_mutexattr_getrobust_np() 会返回 0。其他任何返回值都表示出现了错误。
如果出现以下任一情况,pthread_mutexattr_getrobust_np() 将失败并返回对应的值。
ENOSYS
描述:选项 _POSIX_THREAD_PRIO__INHERIT 未定义,或者该实现不支持 pthread_mutexattr_getrobust_np()。
pthread_mutexattr_getrobust_np() 可能会在出现以下情况时失败:
EINVAL
描述:attr 或 robustness 指定的值无效。
表 4–3 列出了用来处理互斥锁的函数。
表 4–3 互斥锁的例程
操作 |
相关函数说明 |
---|---|
初始化互斥锁 |
pthread_mutex_init 语法 |
使互斥锁保持一致 |
pthread_mutex_consistent_np 语法 |
锁定互斥锁 |
pthread_mutex_lock 语法 |
解除锁定互斥锁 |
pthread_mutex_unlock 语法 |
使用非阻塞互斥锁锁定 |
pthread_mutex_trylock 语法 |
销毁互斥锁 |
pthread_mutex_destroy 语法 |
缺省调度策略 SCHED_OTHER 不指定线程可以获取锁的顺序。如果多个线程正在等待一个互斥锁,则获取顺序是不确定的。出现争用时,缺省行为是按优先级顺序解除线程的阻塞。
使用 pthread_mutex_init(3C) 可以使用缺省值初始化由 mp 所指向的互斥锁,还可以指定已经使用 pthread_mutexattr_init() 设置的互斥锁属性。mattr 的缺省值为 NULL。对于 Solaris 线程,请参见mutex_init(3C) 语法。
int pthread_mutex_init(pthread_mutex_t *mp, const pthread_mutexattr_t *mattr);
#includepthread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER; pthread_mutexattr_t mattr; int ret; /* initialize a mutex to its default value */ ret = pthread_mutex_init(&mp, NULL); /* initialize a mutex */ ret = pthread_mutex_init(&mp, &mattr);
如果互斥锁已初始化,则它会处于未锁定状态。互斥锁可以位于进程之间共享的内存中或者某个进程的专用内存中。
注 –
初始化互斥锁之前,必须将其所在的内存清零。
将 mattr 设置为 NULL 的效果与传递缺省互斥锁属性对象的地址相同,但是没有内存开销。
使用 PTHREAD_MUTEX_INITIALIZER 宏可以将以静态方式定义的互斥锁初始化为其缺省属性。
当其他线程正在使用某个互斥锁时,请勿重新初始化或销毁该互斥锁。如果任一操作没有正确完成,将会导致程序失败。如果要重新初始化或销毁某个互斥锁,则应用程序必须确保当前未使用该互斥锁。
pthread_mutex_init() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EBUSY
描述:该实现已检测到系统尝试重新初始化 mp 所引用的对象,即以前进行过初始化但 尚未销毁的互斥锁。
EINVAL
描述:mattr 属性值无效。互斥锁尚未修改。
EFAULT
描述:mp 所指向的互斥锁的地址无效。
如果某个互斥锁的属主失败,该互斥锁可能会变为不一致。
使用 pthread_mutex_consistent_np 可使互斥对象 mutex 在其属主停止之后保持一致。
#includeint pthread_mutex_consistent_np(pthread_mutex_t *mutex);
注 –
仅当定义了 _POSIX_THREAD_PRIO_INHERIT 符号时,pthread_mutex_consistent_np() 才适用,并且仅适用于使用协议属性值 PTHREAD_PRIO_INHERIT 初始化的互斥锁。
调用 pthread_mutex_lock() 会获取不一致的互斥锁。EOWNWERDEAD 返回值表示出现不一致的互斥锁。
持有以前通过调用 pthread_mutex_lock() 获取的互斥锁时可调用 pthread_mutex_consistent_np()。
如果互斥锁的属主失败,则该互斥锁保护的临界段可能会处于不一致状态。在这种情况下,仅当互斥锁保护的临界段可保持一致时,才能使该互斥锁保持一致。
针对互斥锁调用 pthread_mutex_lock()、pthread_mutex_unlock() 和 pthread_mutex_trylock() 会以正常方式进行。
对于不一致或者未持有的互斥锁,pthread_mutex_consistent_np() 的行为是不确定的。
pthread_mutex_consistent_np() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。
pthread_mutex_consistent_np() 会在出现以下情况时失败:
ENOSYS
描述:选项 _POSIX_THREAD_PRIO_INHERIT 未定义,或者该实现不支持 pthread_mutex_consistent_np()。
pthread_mutex_consistent_np() 可能会在出现以下情况时失败:
EINVAL
描述:mattr 属性值无效。
使用 pthread_mutex_lock(3C) 可以锁定 mutex 所指向的互斥锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
#includepthread_mutex_t mutex; int ret; ret = pthread_ mutex_lock(&mp); /* acquire the mutex */
当 pthread_mutex_lock() 返回时,该互斥锁已被锁定。调用线程是该互斥锁的属主。如果该互斥锁已被另一个线程锁定和拥有,则调用线程将阻塞,直到该互斥锁变为可用为止。 对于 Solaris 线程,请参见mutex_lock 语法。
如果互斥锁类型为 PTHREAD_MUTEX_NORMAL,则不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或未锁定,则将产生不确定的行为。
如果互斥锁类型为 PTHREAD_MUTEX_ERRORCHECK,则会提供错误检查。如果某个线程尝试重新锁定的互斥锁已经由该线程锁定,则将返回错误。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
如果互斥锁类型为 PTHREAD_MUTEX_RECURSIVE,则该互斥锁会保留锁定计数这一概念。线程首次成功获取互斥锁时,锁定计数会设置为 1。线程每重新锁定该互斥锁一次,锁定计数就增加 1。线程每解除锁定该互斥锁一次,锁定计数就减小 1。 锁定计数达到 0 时,该互斥锁即可供其他线程获取。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
如果互斥锁类型是 PTHREAD_MUTEX_DEFAULT,则尝试以递归方式锁定该互斥锁将产生不确定的行为。对于不是由调用线程锁定的互斥锁,如果尝试解除对它的锁定,则会产生不确定的行为。如果尝试解除锁定尚未锁定的互斥锁,则会产生不确定的行为。
pthread_mutex_lock() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EAGAIN
描述:由于已超出了互斥锁递归锁定的最大次数,因此无法获取该互斥锁。
EDEADLK
描述:当前线程已经拥有互斥锁。
如果定义了 _POSIX_THREAD_PRIO_INHERIT 符号,则会使用协议属性值 PTHREAD_PRIO_INHERIT 对互斥锁进行初始化。此外,如果 pthread_mutexattr_setrobust_np() 的 robustness 参数是 PTHREAD_MUTEX_ROBUST_NP,则该函数将失败并返回以下值之一:
EOWNERDEAD
描述:该互斥锁的最后一个属主在持有该互斥锁时失败。该互斥锁现在由调用方拥有。调用方必须尝试使该互斥锁所保护的状态一致。
如果调用方能够使状态保持一致,请针对该互斥锁调用 pthread_mutex_consistent_np() 并解除锁定该互斥锁。以后对 pthread_mutex_lock() 的调用都将正常进行。
如果调用方无法使状态保持一致,请勿针对该互斥锁调用 pthread_mutex_init(),但要解除锁定该互斥锁。以后调用 pthread_mutex_lock() 时将无法获取该互斥锁,并且将返回错误代码 ENOTRECOVERABLE。
如果获取该锁的属主失败并返回 EOWNERDEAD,则下一个属主获取该锁时将返回 EOWNERDEAD。
ENOTRECOVERABLE
描述:尝试获取的互斥锁正在保护某个状态,此状态由于该互斥锁以前的属主在持有该锁时失败而导致不可恢复。尚未获取该互斥锁。如果满足以下条件,则可能出现此不可恢复的情况:
以前获取该锁时返回 EOWNERDEAD
该属主无法清除此状态
该属主已经解除锁定了该互斥锁,但是没有使互斥锁状态保持一致
ENOMEM
描述:已经超出了可同时持有的互斥锁数目的限制。
使用 pthread_mutex_unlock(3C) 可以解除锁定 mutex 所指向的互斥锁。 对于 Solaris 线程,请参见mutex_unlock 语法。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
#includepthread_mutex_t mutex; int ret; ret = pthread_mutex_unlock(&mutex); /* release the mutex */
pthread_mutex_unlock() 可释放 mutex 引用的互斥锁对象。互斥锁的释放方式取决于互斥锁的类型属性。 如果调用 pthread_mutex_unlock() 时有多个线程被 mutex 对象阻塞,则互斥锁变为可用时调度策略可确定获取该互斥锁的线程。 对于 PTHREAD_MUTEX_RECURSIVE 类型的互斥锁,当计数达到零并且调用线程不再对该互斥锁进行任何锁定时,该互斥锁将变为可用。
pthread_mutex_unlock() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EPERM
描述:当前线程不拥有互斥锁。
使用 pthread_mutex_trylock(3C) 可以尝试锁定 mutex 所指向的互斥锁。对于 Solaris 线程,请参见mutex_trylock 语法。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
#includepthread_mutex_t mutex; int ret; ret = pthread_mutex_trylock(&mutex); /* try to lock the mutex */
pthread_mutex_trylock() 是 pthread_mutex_lock() 的非阻塞版本。如果 mutex 所引用的互斥对象当前被任何线程(包括当前线程)锁定,则将立即返回该调用。否则,该互斥锁将处于锁定状态,调用线程是其属主。
pthread_mutex_trylock() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EBUSY
描述:由于 mutex 所指向的互斥锁已锁定,因此无法获取该互斥锁。
EAGAIN
描述:由于已超出了 mutex 的递归锁定最大次数,因此无法获取该互斥锁。
如果定义了 _POSIX_THREAD_PRIO_INHERIT 符号,则会使用协议属性值 PTHREAD_PRIO_INHERIT 对互斥锁进行初始化。此外,如果 pthread_mutexattr_setrobust_np() 的 robustness 参数是 PTHREAD_MUTEX_ROBUST_NP,则该函数将失败并返回以下值之一:
EOWNERDEAD
描述:该互斥锁的最后一个属主在持有该互斥锁时失败。该互斥锁现在由调用方拥有。调用方必须尝试使该互斥锁所保护的状态一致。
如果调用方能够使状态保持一致,请针对该互斥锁调用 pthread_mutex_consistent_np() 并解除锁定该互斥锁。以后对 pthread_mutex_lock() 的调用都将正常进行。
如果调用方无法使状态保持一致,请勿针对该互斥锁调用 pthread_mutex_init(),而要解除锁定该互斥锁。以后调用 pthread_mutex_trylock() 时将无法获取该互斥锁,并且将返回错误代码 ENOTRECOVERABLE。
如果已获取该锁的属主失败并返回 EOWNERDEAD,则下一个属主获取该锁时返回 EOWNERDEAD。
ENOTRECOVERABLE
描述:尝试获取的互斥锁正在保护某个状态,此状态由于该互斥锁以前的属主在持有该锁时失败而导致不可恢复。尚未获取该互斥锁。 以下条件下可能会出现此情况:
以前获取该锁时返回 EOWNERDEAD
该属主无法清除此状态
该属主已经解除锁定了该互斥锁,但是没有使互斥锁状态保持一致
ENOMEM
描述:已经超出了可同时持有的互斥锁数目的限制。
使用 pthread_mutex_destroy(3C) 可以销毁与 mp 所指向的互斥锁相关联的任何状态。 对于 Solaris 线程,请参见mutex_destroy 语法。
int pthread_mutex_destroy(pthread_mutex_t *mp);
#includepthread_mutex_t mp; int ret; ret = pthread_mutex_destroy(&mp); /* mutex is destroyed */
请注意,没有释放用来存储互斥锁的空间。
pthread_mutex_destroy() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述:mp 指定的值不会引用已初始化的互斥锁对象。
示例 4–1 显示了使用互斥锁定的一些代码段。
#includepthread_mutex_t count_mutex; long long count; void increment_count() { pthread_mutex_lock(&count_mutex); count = count + 1; pthread_mutex_unlock(&count_mutex); } long long get_count() { long long c; pthread_mutex_lock(&count_mutex); c = count; pthread_mutex_unlock(&count_mutex); return (c); }
示例 4–1 中的两个函数将互斥锁用于不同目的。increment_count() 函数使用互斥锁确保对共享变量进行原子更新。get_count() 函数使用互斥锁保证以原子方式读取 64 位数量 count。在 32 位体系结构上,long
long
实际上是两个 32 位数量。
读取整数值时执行的是原子运算,因为整数是大多数计算机中常见的字长。
有时,可能需要同时访问两个资源。您可能正在使用其中的一个资源,随后发现还需要另一个资源。如果两个线程尝试声明这两个资源,但是以不同的顺序锁定与这些资源相关联的互斥锁,则会出现问题。例如,如果两个线程分别锁定互斥锁 1 和互斥锁 2,则每个线程尝试锁定另一个互斥锁时,将会出现死锁。示例 4–2 说明了可能的死锁情况。
线程 1 |
线程 2 |
|
---|---|---|
pthread_mutex_lock(&m1);
/* use resource 1 */
pthread_mutex_lock(&m2);
/* use resources 1 and 2 */ pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); |
pthread_mutex_lock(&m2);
/* use resource 2 */
pthread_mutex_lock(&m1);
/* use resources 1 and 2 */ pthread_mutex_unlock(&m1); pthread_mutex_unlock(&m2); |
|
避免此问题的最佳方法是,确保线程在锁定多个互斥锁时,以同样的顺序进行锁定。如果始终按照规定的顺序锁定,就不会出现死锁。此方法称为锁分层结构,它通过为互斥锁指定逻辑编号来对这些锁进行排序。
另外,请注意以下限制:如果您持有的任何互斥锁其指定编号大于 n,则不能提取指定编号为 n 的互斥锁。
但是,不能始终使用此方法。有时,必须按照与规定不同的顺序提取互斥锁。要防止在这种情况下出现死锁,请使用 pthread_mutex_trylock()。如果线程发现无法避免死锁时,该线程必须释放其互斥锁。
线程 1 |
线程 2 |
---|---|
pthread_mutex_lock(&m1); pthread_mutex_lock(&m2);
/* no processing */
pthread_mutex_unlock(&m2); pthread_mutex_unlock(&m1); |
for (; ;) { pthread_mutex_lock(&m2);
if(pthread_mutex_trylock(&m1)==0) /* got it */ break; /* didn't get it */ pthread_mutex_unlock(&m2); } /* get locks; no processing */ pthread_mutex_unlock(&m1); pthread_mutex_unlock(&m2); |
在示例 4–3 中,线程 1 按照规定的顺序锁定互斥锁,但是线程 2 不按顺序提取互斥锁。要确保不会出现死锁,线程 2 必须非常小心地提取互斥锁 1。如果线程 2 在等待该互斥锁释放时被阻塞,则线程 2 可能刚才已经与线程 1 进入了死锁状态。
要确保线程 2 不会进入死锁状态,线程 2 需要调用 pthread_mutex_trylock(),此函数可在该互斥锁可用时提取它。如果该互斥锁不可用,线程 2 将立即返回并报告提取失败。此时,线程 2 必须释放互斥锁 2。线程 1 现在会锁定互斥锁 2,然后释放互斥锁 1 和互斥锁 2。
示例 4–4 和示例 4–5 说明了如何同时提取三个锁。通过按照规定的顺序提取锁可避免出现死锁。
typedef struct node1 { int value; struct node1 *link; pthread_mutex_t lock; } node1_t; node1_t ListHead;
本示例针对每个包含一个互斥锁的节点使用单链接列表结构。要将某个节点从列表中删除,请首先从 ListHead 开始搜索列表,直到找到所需的节点为止。ListHead 永远不会被删除。
要防止执行此搜索时产生并发删除,请在访问每个节点的任何内容之前先锁定该节点。由于所有的搜索都从 ListHead 开始,并且始终按照列表中的顺序提取锁,因此不会出现死锁。
因为更改涉及到两个节点,所以找到所需的节点之后,请锁定该节点及其前序节点。因为前序节点的锁总是最先提取,所以可再次防止出现死锁。示例 4–5 说明如何使用 C 代码来删除单链接列表中的项。
node1_t *delete(int value) { node1_t *prev, *current; prev = &ListHead; pthread_mutex_lock(&prev->lock); while ((current = prev->link) != NULL) { pthread_mutex_lock(¤t->lock); if (current->value == value) { prev->link = current->link; pthread_mutex_unlock(¤t->lock); pthread_mutex_unlock(&prev->lock); current->link = NULL; return(current); } pthread_mutex_unlock(&prev->lock); prev = current; } pthread_mutex_unlock(&prev->lock); return(NULL); }
示例 4–6 通过将以前的列表结构转换为循环列表来对其进行修改。由于不再存在用于标识的头节点,因该线程可以与特定的节点相关联,并可针对该节点及其邻居执行操作。锁分层结构在此处不适用,因为链接之后的分层结构明显是循环结构。
typedef struct node2 { int value; struct node2 *link; pthread_mutex_t lock; } node2_t;
以下的 C 代码用来获取两个节点上的锁并执行涉及到这两个锁的操作。
void Hit Neighbor(node2_t *me) { while (1) { pthread_mutex_lock(&me->lock); if (pthread_mutex_lock(&me->link->lock)!= 0) { /* failed to get lock */ pthread_mutex_unlock(&me->lock); continue; } break; } me->link->value += me->value; me->value /=2; pthread_mutex_unlock(&me->link->lock); pthread_mutex_unlock(&me->lock); }
使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用。
使用条件变量,线程可以以原子方式阻塞,直到满足某个条件为止。对条件的测试是在互斥锁(互斥)的保护下进行的。
如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作:
唤醒
再次获取互斥锁
重新评估条件
在以下情况下,条件变量可用于在进程之间同步线程:
线程是在可以写入的内存中分配的
内存由协作进程共享
调度策略可确定唤醒阻塞线程的方式。对于缺省值 SCHED_OTHER,将按优先级顺序唤醒线程。
必须设置和初始化条件变量的属性,然后才能使用条件变量。表 4–4 列出了用于处理条件变量属性的函数。
表 4–4 条件变量属性
操作 |
函数说明 |
---|---|
初始化条件变量属性 |
pthread_condattr_init 语法 |
删除条件变量属性 |
pthread_condattr_destroy 语法 |
设置条件变量的范围 |
pthread_condattr_setpshared 语法 |
获取条件变量的范围 |
pthread_condattr_getpshared 语法 |
表 4–5 中显示了定义条件变量的范围时 Solaris 线程和 POSIX 线程之间的差异。
表 4–5 条件变量范围比较
Solaris |
POSIX |
定义 |
---|---|---|
USYNC_PROCESS |
PTHREAD_PROCESS_SHARED |
用于同步该进程和其他进程中的线程 |
USYNC_THREAD |
PTHREAD_PROCESS_PRIVATE |
用于仅同步该进程中的线程 |
使用 pthread_condattr_init(3C) 可以将与该对象相关联的属性初始化为其缺省值。在执行过程中,线程系统会为每个属性对象分配存储空间。
int pthread_condattr_init(pthread_condattr_t *cattr);
#includepthread_condattr_t cattr; int ret; /* initialize an attribute to default value */ ret = pthread_condattr_init(&cattr);
调用此函数时,pshared 属性的缺省值为 PTHREAD_PROCESS_PRIVATE。pshared 的该值表示可以在进程内使用已初始化的条件变量。
cattr 的数据类型为 opaque,其中包含一个由系统分配的属性对象。cattr 范围可能的值为 PTHREAD_PROCESS_PRIVATE 和 PTHREAD_PROCESS_SHARED。PTHREAD_PROCESS_PRIVATE 是缺省值。
条件变量属性必须首先由 pthread_condattr_destroy(3C) 重新初始化后才能重用。pthread_condattr_init() 调用会返回指向类型为 opaque 的对象的指针。如果未销毁该对象,则会导致内存泄漏。
pthread_condattr_init() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
ENOMEM
描述:分配的内存不足,无法初始化线程属性对象。
EINVAL
描述:cattr 指定的值无效。
使用 pthread_condattr_destroy(3C) 可以删除存储并使属性对象无效。
int pthread_condattr_destroy(pthread_condattr_t *cattr);
#includepthread_condattr_t cattr; int ret; /* destroy an attribute */ ret = pthread_condattr_destroy(&cattr);
pthread_condattr_destroy() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:cattr 指定的值无效。
pthread_condattr_setpshared(3C) 可用来将条件变量的范围设置为进程专用(进程内)或系统范围内(进程间)。
int pthread_condattr_setpshared(pthread_condattr_t *cattr, int pshared);
#includepthread_condattr_t cattr; int ret; /* all processes */ ret = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED); /* within a process */ ret = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_PRIVATE);
如果 pshared 属性在共享内存中设置为 PTHREAD_PROCESS_SHARED,则其所创建的条件变量可以在多个进程中的线程之间共享。此行为与最初的 Solaris 线程实现中 mutex_init() 中的 USYNC_PROCESS 标志等效。
如果互斥锁的 pshared 属性设置为 PTHREAD_PROCESS_PRIVATE,则仅有那些由同一个进程创建的线程才能够处理该互斥锁。PTHREAD_PROCESS_PRIVATE 是缺省值。PTHREAD_PROCESS_PRIVATE 所产生的行为与在最初的 Solaris 线程的 cond_init() 调用中使用 USYNC_THREAD 标志相同。PTHREAD_PROCESS_PRIVATE 的行为与局部条件变量相同。PTHREAD_PROCESS_SHARED 的行为与全局条件变量等效。
pthread_condattr_setpshared() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:cattr 或 pshared 的值无效。
pthread_condattr_getpshared(3C) 可用来获取属性对象 cattr 的 pshared 的当前值。
int pthread_condattr_getpshared(const pthread_condattr_t *cattr, int *pshared);
#includepthread_condattr_t cattr; int pshared; int ret; /* get pshared value of condition variable */ ret = pthread_condattr_getpshared(&cattr, &pshared);
属性对象的值为 PTHREAD_PROCESS_SHARED 或 PTHREAD_PROCESS_PRIVATE。
pthread_condattr_getpshared() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:cattr 的值无效。
本节介绍如何使用条件变量。表 4–6 列出了可用的函数。
表 4–6 条件变量函数
操作 |
相关函数说明 |
---|---|
初始化条件变量 |
pthread_cond_init 语法 |
基于条件变量阻塞 |
pthread_cond_wait 语法 |
解除阻塞特定线程 |
pthread_cond_signal 语法 |
在指定的时间之前阻塞 |
pthread_cond_timedwait 语法 |
在指定的时间间隔内阻塞 |
pthread_cond_reltimedwait_np 语法 |
解除阻塞所有线程 |
pthread_cond_broadcast 语法 |
销毁条件变量状态 |
pthread_cond_destroy 语法 |
使用 pthread_cond_init(3C) 可以将 cv 所指示的条件变量初始化为其缺省值,或者指定已经使用 pthread_condattr_init() 设置的条件变量属性。
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);
#includepthread_cond_t cv; pthread_condattr_t cattr; int ret; /* initialize a condition variable to its default value */ ret = pthread_cond_init(&cv, NULL); /* initialize a condition variable */ ret = pthread_cond_init(&cv, &cattr);
cattr 设置为 NULL。将 cattr 设置为 NULL 与传递缺省条件变量属性对象的地址等效,但是没有内存开销。对于 Solaris 线程,请参见cond_init 语法。
使用 PTHREAD_COND_INITIALIZER 宏可以将以静态方式定义的条件变量初始化为其缺省属性。PTHREAD_COND_INITIALIZER 宏与动态分配具有 null 属性的 pthread_cond_init() 等效,但是不进行错误检查。
多个线程决不能同时初始化或重新初始化同一个条件变量。如果要重新初始化或销毁某个条件变量,则应用程序必须确保该条件变量未被使用。
pthread_cond_init() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述:cattr 指定的值无效。
EBUSY
描述:条件变量处于使用状态。
EAGAIN
描述:必要的资源不可用。
ENOMEM
描述:内存不足,无法初始化条件变量。
使用 pthread_cond_wait(3C) 可以以原子方式释放 mp 所指向的互斥锁,并导致调用线程基于 cv 所指向的条件变量阻塞。对于 Solaris 线程,请参见cond_wait 语法。
int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex);
#includepthread_cond_t cv; pthread_mutex_t mp; int ret; /* wait on condition variable */ ret = pthread_cond_wait(&cv, &mp);
阻塞的线程可以通过 pthread_cond_signal() 或 pthread_cond_broadcast() 唤醒,也可以在信号传送将其中断时唤醒。
不能通过 pthread_cond_wait() 的返回值来推断与条件变量相关联的条件的值的任何变化。必须重新评估此类条件。
pthread_cond_wait() 例程每次返回结果时调用线程都会锁定并且拥有互斥锁,即使返回错误时也是如此。
该条件获得信号之前,该函数一直被阻塞。该函数会在被阻塞之前以原子方式释放相关的互斥锁,并在返回之前以原子方式再次获取该互斥锁。
通常,对条件表达式的评估是在互斥锁的保护下进行的。如果条件表达式为假,线程会基于条件变量阻塞。然后,当该线程更改条件值时,另一个线程会针对条件变量发出信号。这种变化会导致所有等待该条件的线程解除阻塞并尝试再次获取互斥锁。
必须重新测试导致等待的条件,然后才能从 pthread_cond_wait() 处继续执行。唤醒的线程重新获取互斥锁并从 pthread_cond_wait() 返回之前,条件可能会发生变化。等待线程可能并未真正唤醒。建议使用的测试方法是,将条件检查编写为调用 pthread_cond_wait() 的 while() 循环。
pthread_mutex_lock(); while(condition_is_false) pthread_cond_wait(); pthread_mutex_unlock();
如果有多个线程基于该条件变量阻塞,则无法保证按特定的顺序获取互斥锁。
注 –
pthread_cond_wait() 是取消点。如果取消处于暂挂状态,并且调用线程启用了取消功能,则该线程会终止,并在继续持有该锁的情况下开始执行清除处理程序。
pthread_cond_wait() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:cv 或 mp 指定的值无效。
对于基于 cv 所指向的条件变量阻塞的线程,使用 pthread_cond_signal(3C) 可以解除阻塞该线程。对于 Solaris 线程,请参见cond_signal 语法。
int pthread_cond_signal(pthread_cond_t *cv);
#includepthread_cond_t cv; int ret; /* one condition variable is signaled */ ret = pthread_cond_signal(&cv);
应在互斥锁的保护下修改相关条件,该互斥锁用于获得信号的条件变量中。否则,可能在条件变量的测试和 pthread_cond_wait() 阻塞之间修改该变量,这会导致无限期等待。
调度策略可确定唤醒阻塞线程的顺序。对于 SCHED_OTHER,将按优先级顺序唤醒线程。
如果没有任何线程基于条件变量阻塞,则调用 pthread_cond_signal() 不起作用。
pthread_mutex_t count_lock; pthread_cond_t count_nonzero; unsigned count; decrement_count() { pthread_mutex_lock(&count_lock); while (count == 0) pthread_cond_wait(&count_nonzero, &count_lock); count = count - 1; pthread_mutex_unlock(&count_lock); } increment_count() { pthread_mutex_lock(&count_lock); if (count == 0) pthread_cond_signal(&count_nonzero); count = count + 1; pthread_mutex_unlock(&count_lock); }
pthread_cond_signal() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:cv 指向的地址非法。
示例 4–8 说明了如何使用 pthread_cond_wait() 和 pthread_cond_signal()。
pthread_cond_timedwait(3C) 的用法与 pthread_cond_wait() 的用法基本相同,区别在于在由 abstime 指定的时间之后 pthread_cond_timedwait() 不再被阻塞。
int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *mp, const struct timespec *abstime);
#include#include pthread_cond_t cv; pthread_mutex_t mp; timestruct_t abstime; int ret; /* wait on condition variable */ ret = pthread_cond_timedwait(&cv, &mp, &abstime);
pthread_cond_timewait() 每次返回时调用线程都会锁定并且拥有互斥锁,即使 pthread_cond_timedwait() 返回错误时也是如此。 对于 Solaris 线程,请参见cond_timedwait 语法。
pthread_cond_timedwait() 函数会一直阻塞,直到该条件获得信号,或者最后一个参数所指定的时间已过为止。
注 –
pthread_cond_timedwait() 也是取消点。
pthread_timestruc_t to; pthread_mutex_t m; pthread_cond_t c; ... pthread_mutex_lock(&m); to.tv_sec = time(NULL) + TIMEOUT; to.tv_nsec = 0; while (cond == FALSE) { err = pthread_cond_timedwait(&c, &m, &to); if (err == ETIMEDOUT) { /* timeout, do something */ break; } } pthread_mutex_unlock(&m);
pthread_cond_timedwait() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述:cv 或 abstime 指向的地址非法。
ETIMEDOUT
描述:abstime 指定的时间已过。
超时会指定为当天时间,以便在不重新计算值的情况下高效地重新测试条件,如示例 4–9 中所示。
pthread_cond_reltimedwait_np(3C) 的用法与 pthread_cond_timedwait() 的用法基本相同,唯一的区别在于 pthread_cond_reltimedwait_np() 会采用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。
int pthread_cond_reltimedwait_np(pthread_cond_t *cv, pthread_mutex_t *mp, const struct timespec *reltime);
#include#include pthread_cond_t cv; pthread_mutex_t mp; timestruct_t reltime; int ret; /* wait on condition variable */ ret = pthread_cond_reltimedwait_np(&cv, &mp, &reltime);
pthread_cond_reltimedwait_np() 每次返回时调用线程都会锁定并且拥有互斥锁,即使 pthread_cond_reltimedwait_np() 返回错误时也是如此。对于 Solaris 线程,请参见 cond_reltimedwait(3C)。pthread_cond_reltimedwait_np() 函数会一直阻塞,直到该条件获得信号,或者最后一个参数指定的时间间隔已过为止。
注 –
pthread_cond_reltimedwait_np() 也是取消点。
pthread_cond_reltimedwait_np() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述:cv 或 reltime 指示的地址非法。
ETIMEDOUT
描述:reltime 指定的时间间隔已过。
对于基于 cv 所指向的条件变量阻塞的线程,使用 pthread_cond_broadcast(3C) 可以解除阻塞所有这些线程,这由 pthread_cond_wait() 来指定。
int pthread_cond_broadcast(pthread_cond_t *cv);
#includepthread_cond_t cv; int ret; /* all condition variables are signaled */ ret = pthread_cond_broadcast(&cv);
如果没有任何线程基于该条件变量阻塞,则调用 pthread_cond_broadcast() 不起作用。对于 Solaris 线程,请参见cond_broadcast 语法。
由于 pthread_cond_broadcast() 会导致所有基于该条件阻塞的线程再次争用互斥锁,因此请谨慎使用 pthread_cond_broadcast()。例如,通过使用 pthread_cond_broadcast(),线程可在资源释放后争用不同的资源量,如示例 4–10 中所示。
pthread_mutex_t rsrc_lock; pthread_cond_t rsrc_add; unsigned int resources; get_resources(int amount) { pthread_mutex_lock(&rsrc_lock); while (resources < amount) { pthread_cond_wait(&rsrc_add, &rsrc_lock); } resources -= amount; pthread_mutex_unlock(&rsrc_lock); } add_resources(int amount) { pthread_mutex_lock(&rsrc_lock); resources += amount; pthread_cond_broadcast(&rsrc_add); pthread_mutex_unlock(&rsrc_lock); }
请注意,在 add_resources() 中,首先更新 resources 还是首先在互斥锁中调用 pthread_cond_broadcast() 无关紧要。
应在互斥锁的保护下修改相关条件,该互斥锁用于获得信号的条件变量中。否则,可能在条件变量的测试和 pthread_cond_wait() 阻塞之间修改该变量,这会导致无限期等待。
pthread_cond_broadcast() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:cv 指示的地址非法。
使用 pthread_cond_destroy(3C) 可以销毁与 cv 所指向的条件变量相关联的任何状态。对于 Solaris 线程,请参见cond_destroy 语法。
int pthread_cond_destroy(pthread_cond_t *cv);
#includepthread_cond_t cv; int ret; /* Condition variable is destroyed */ ret = pthread_cond_destroy(&cv);
请注意,没有释放用来存储条件变量的空间。
pthread_cond_destroy() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:cv 指定的值无效。
如果线程未持有与条件相关联的互斥锁,则调用 pthread_cond_signal() 或 pthread_cond_broadcast() 会产生唤醒丢失错误。
满足以下所有条件时,即会出现唤醒丢失问题:
一个线程调用 pthread_cond_signal() 或 pthread_cond_broadcast()
另一个线程已经测试了该条件,但是尚未调用 pthread_cond_wait()
没有正在等待的线程
信号不起作用,因此将会丢失
仅当修改所测试的条件但未持有与之相关联的互斥锁时,才会出现此问题。只要仅在持有关联的互斥锁同时修改所测试的条件,即可调用 pthread_cond_signal() 和 pthread_cond_broadcast(),而无论这些函数是否持有关联的互斥锁。
并发编程中收集了许多标准的众所周知的问题,生成方和使用者问题只是其中的一个问题。此问题涉及到一个大小限定的缓冲区和两类线程(生成方和使用者),生成方将项放入缓冲区中,然后使用者从缓冲区中取走项。
生成方必须在缓冲区中有可用空间之后才能向其中放置内容。使用者必须在生成方向缓冲区中写入之后才能从中提取内容。
条件变量表示一个等待某个条件获得信号的线程队列。
示例 4–11 中包含两个此类队列。一个队列 (less) 针对生成方,用于等待缓冲区中出现空位置。另一个队列 (more) 针对使用者,用于等待从缓冲槽位的空位置中提取其中包含的信息。该示例中还包含一个互斥锁,因为描述该缓冲区的数据结构一次只能由一个线程访问。
typedef struct { char buf[BSIZE]; int occupied; int nextin; int nextout; pthread_mutex_t mutex; pthread_cond_t more; pthread_cond_t less; } buffer_t; buffer_t buffer;
如示例 4–12 中所示,生成方线程获取该互斥锁以保护 buffer 数据结构,然后,缓冲区确定是否有空间可用于存放所生成的项。如果没有可用空间,生成方线程会调用 pthread_cond_wait()。pthread_cond_wait() 会导致生成方线程连接正在等待 less 条件获得信号的线程队列。less 表示缓冲区中的可用空间。
与此同时,在调用 pthread_cond_wait() 的过程中,该线程会释放互斥锁的锁定。正在等待的生成方线程依赖于使用者线程在条件为真时发出信号,如示例 4–12 中所示。该条件获得信号时,将会唤醒等待 less 的第一个线程。但是,该线程必须再次锁定互斥锁,然后才能从 pthread_cond_wait() 返回。
获取互斥锁可确保该线程再次以独占方式访问缓冲区的数据结构。该线程随后必须检查缓冲区中是否确实存在可用空间。如果空间可用,该线程会向下一个可用的空位置中进行写入。
与此同时,使用者线程可能正在等待项出现在缓冲区中。这些线程正在等待条件变量 more。刚在缓冲区中存储内容的生成方线程会调用 pthread_cond_signal() 以唤醒下一个正在等待的使用者。如果没有正在等待的使用者,此调用将不起作用。
最后,生成方线程会解除锁定互斥锁,从而允许其他线程处理缓冲区的数据结构。
void producer(buffer_t *b, char item) { pthread_mutex_lock(&b->mutex); while (b->occupied >= BSIZE) pthread_cond_wait(&b->less, &b->mutex); assert(b->occupied < BSIZE); b->buf[b->nextin++] = item; b->nextin %= BSIZE; b->occupied++; /* now: either b->occupied < BSIZE and b->nextin is the index of the next empty slot in the buffer, or b->occupied == BSIZE and b->nextin is the index of the next (occupied) slot that will be emptied by a consumer (such as b->nextin == b->nextout) */ pthread_cond_signal(&b->more); pthread_mutex_unlock(&b->mutex); }
请注意 assert() 语句的用法。除非在编译代码时定义了 NDEBUG,否则 assert() 在其参数的计算结果为真(非零)时将不执行任何操作。如果参数的计算结果为假(零),则该程序会中止。在多线程程序中,此类断言特别有用。如果断言失败,assert() 会立即指出运行时问题。assert() 还有另一个作用,即提供有用的注释。
以 /* now: either b->occupied ... 开头的注释最好以断言形式表示,但是由于语句过于复杂,无法用布尔值表达式来表示,因此将用英语表示。
断言和注释都是不变量的示例。这些不变量是逻辑语句,在程序正常执行时不应将其声明为假,除非是线程正在修改不变量中提到的一些程序变量时的短暂修改过程中。当然,只要有线程执行语句,断言就应当为真。
使用不变量是一种极为有用的方法。即使没有在程序文本中声明不变量,在分析程序时也应将其视为不变量。
每次线程执行包含注释的代码时,生成方代码中表示为注释的不变量始终为真。如果将此注释移到紧挨 mutex_unlock() 的后面,则注释不一定仍然为真。如果将此注释移到紧跟 assert() 之后的位置,则注释仍然为真。
因此,不变量可用于表示一个始终为真的属性,除非一个生成方或一个使用者正在更改缓冲区的状态。线程在互斥锁的保护下处理缓冲区时,该线程可能会暂时声明不变量为假。但是,一旦线程结束对缓冲区的操作,不变量即会恢复为真。
示例 4–13 给出了使用者的代码。该逻辑流程与生成方的逻辑流程相对称。
char consumer(buffer_t *b) { char item; pthread_mutex_lock(&b->mutex); while(b->occupied <= 0) pthread_cond_wait(&b->more, &b->mutex); assert(b->occupied > 0); item = b->buf[b->nextout++]; b->nextout %= BSIZE; b->occupied--; /* now: either b->occupied > 0 and b->nextout is the index of the next occupied slot in the buffer, or b->occupied == 0 and b->nextout is the index of the next (empty) slot that will be filled by a producer (such as b->nextout == b->nextin) */ pthread_cond_signal(&b->less); pthread_mutex_unlock(&b->mutex); return(item); }
信号是 E. W. Dijkstra 在二十世纪六十年代末设计的一种编程架构。Dijkstra 的模型与铁路操作有关:假设某段铁路是单线的,因此一次只允许一列火车通过。
信号将用于同步通过该轨道的火车。火车在进入单一轨道之前必须等待信号灯变为允许通行的状态。火车进入轨道后,会改变信号状态,防止其他火车进入该轨道。火车离开这段轨道时,必须再次更改信号的状态,以便允许其他火车进入轨道。
在计算机版本中,信号以简单整数来表示。线程等待获得许可以便继续运行,然后发出信号,表示该线程已经通过针对信号执行 P 操作来继续运行。
线程必须等到信号的值为正,然后才能通过将信号值减 1 来更改该值。完成此操作后,线程会执行 V 操作,即通过将信号值加 1 来更改该值。这些操作必须以原子方式执行,不能再将其划分成子操作,即,在这些子操作之间不能对信号执行其他操作。在 P 操作中,信号值在减小之前必须为正,从而确保生成的信号值不为负,并且比该值减小之前小 1。
在 P 和 V 操作中,必须在没有干扰的情况下进行运算。如果针对同一信号同时执行两个 V 操作,则实际结果是信号的新值比原来大 2。
对于大多数人来说,如同记住 Dijkstra 是荷兰人一样,记住 P 和 V 本身的含义并不重要。但是,从真正学术的角度来说,P 代表 prolagen,这是由 proberen te verlagen 演变而来的杜撰词,其意思是尝试减小。V 代表 verhogen,其意思是增加。Dijkstra 的技术说明 EWD 74 中介绍了这些含义。
sem_wait(3RT) 和 sem_post(3RT) 分别与 Dijkstra 的 P 和 V 操作相对应。sem_trywait(3RT) 是 P 操作的一种条件形式。如果调用线程不等待就不能减小信号的值,则该调用会立即返回一个非零值。
有两种基本信号:二进制信号和计数信号量。二进制信号的值只能是 0 或 1,计数信号量可以是任意非负值。二进制信号在逻辑上相当于一个互斥锁。
不过,尽管不会强制,但互斥锁应当仅由持有该锁的线程来解除锁定。因为不存在“持有信号的线程”这一概念,所以,任何线程都可以执行 V 或 sem_post(3RT) 操作。
计数信号量与互斥锁一起使用时的功能几乎与条件变量一样强大。在许多情况下,使用计数信号量实现的代码比使用条件变量实现的代码更为简单,如示例 4–14、示例 4–15 和示例 4–16 中所示。
但是,将互斥锁用于条件变量时,会存在一个隐含的括号。该括号可以清楚表明程序受保护的部分。对于信号则不必如此,可以使用并发编程当中的 go to 对其进行调用。信号的功能强大,但是容易以非结构化的不确定方式使用。
POSIX 信号可以是未命名的,也可以是命名的。未命名信号在进程内存中分配,并会进行初始化。未命名信号可能可供多个进程使用,具体取决于信号的分配和初始化的方式。未命名信号可以是通过 fork() 继承的专用信号,也可以通过用来分配和映射这些信号的常规文件的访问保护功能对其进行保护。
命名信号类似于进程共享的信号,区别在于命名信号是使用路径名而非 pshared 值引用的。命名信号可以由多个进程共享。命名信号具有属主用户 ID、组 ID 和保护模式。
对于 open、retrieve、close 和 remove 命名信号,可以使用以下函数:sem_open、sem_getvalue、sem_close 和 sem_unlink。通过使用 sem_open,可以创建一个命名信号,其名称是在文件系统的名称空间中定义的。
有关命名信号的更多信息,请参见 sem_open、sem_getvalue、sem_close 和 sem_unlink 手册页。
从概念上来说,信号量是一个非负整数计数。信号量通常用来协调对资源的访问,其中信号计数会初始化为可用资源的数目。然后,线程在资源增加时会增加计数,在删除资源时会减小计数,这些操作都以原子方式执行。
如果信号计数变为零,则表明已无可用资源。计数为零时,尝试减小信号的线程会被阻塞,直到计数大于零为止。
表 4–7 信号例程
操作 |
相关函数说明 |
---|---|
初始化信号 |
sem_init 语法 |
增加信号 |
sem_post 语法 |
基于信号计数阻塞 |
sem_wait 语法 |
减小信号计数 |
sem_trywait 语法 |
销毁信号状态 |
sem_destroy 语法 |
由于信号无需由同一个线程来获取和释放,因此信号可用于异步事件通知,如用于信号处理程序中。同时,由于信号包含状态,因此可以异步方式使用,而不用象条件变量那样要求获取互斥锁。但是,信号的效率不如互斥锁高。
缺省情况下,如果有多个线程正在等待信号,则解除阻塞的顺序是不确定的。
信号在使用前必须先初始化,但是信号没有属性。
使用 sem_init(3RT) 可以将 sem 所指示的未命名信号变量初始化为 value。
int sem_init(sem_t *sem, int pshared, unsigned int value);
#includesem_t sem; int pshared; int ret; int value; /* initialize a private semaphore */ pshared = 0; value = 1; ret = sem_init(&sem, pshared, value);
如果 pshared 的值为零,则不能在进程之间共享信号。如果 pshared 的值不为零,则可以在进程之间共享信号。对于 Solaris 线程,请参见sema_init 语法。
多个线程决不能初始化同一个信号。
不得对其他线程正在使用的信号重新初始化。
pshared 为 0 时,信号只能由该进程内的所有线程使用。
#includesem_t sem; int ret; int count = 4; /* to be used within this process only */ ret = sem_init(&sem, 0, count);
pshared 不为零时,信号可以由其他进程共享。
#includesem_t sem; int ret; int count = 4; /* to be shared among processes */ ret = sem_init(&sem, 1, count);
sem_init() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述:参数值超过了 SEM_VALUE_MAX。
ENOSPC
描述:初始化信号所需的资源已经用完。到达信号的 SEM_NSEMS_MAX 限制。
ENOSYS
描述:系统不支持 sem_init() 函数。
EPERM
描述:进程缺少初始化信号所需的适当权限。
使用 sem_post(3RT) 可以原子方式增加 sem 所指示的信号。
int sem_post(sem_t *sem);
#includesem_t sem; int ret; ret = sem_post(&sem); /* semaphore is posted */
如果所有线程均基于信号阻塞,则会对其中一个线程解除阻塞。对于 Solaris 线程,请参见sema_post 语法。
sem_post() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:sem 所指示的地址非法。
使用 sem_wait(3RT) 可以阻塞调用线程,直到 sem 所指示的信号计数大于零为止,之后以原子方式减小计数。
int sem_wait(sem_t *sem);
#includesem_t sem; int ret; ret = sem_wait(&sem); /* wait for semaphore */
sem_wait() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述:sem 所指示的地址非法。
EINTR
描述:此函数已被信号中断。
使用 sem_trywait(3RT) 可以在计数大于零时,尝试以原子方式减小 sem 所指示的信号计数。
int sem_trywait(sem_t *sem);
#includesem_t sem; int ret; ret = sem_trywait(&sem); /* try to wait for semaphore*/
此函数是 sem_wait() 的非阻塞版本。sem_trywait() 在失败时会立即返回。
sem_trywait() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述:sem 所指示的地址非法。
EINTR
描述:此函数已被信号中断。
EAGAIN
描述:信号已为锁定状态,因此该信号不能通过 sem_trywait() 操作立即锁定。
使用 sem_destroy(3RT) 可以销毁与 sem 所指示的未命名信号相关联的任何状态。
int sem_destroy(sem_t *sem);
#includesem_t sem; int ret; ret = sem_destroy(&sem); /* the semaphore is destroyed */
不会释放用来存储信号的空间。对于 Solaris 线程,请参见sema_destroy(3C) 语法。
sem_destroy() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述:sem 所指示的地址非法。
示例 4–14 中的数据结构与示例 4–11 中所示的用于条件变量示例的结构类似。两个信号分别表示空缓冲区和满缓冲区的数目,通过这些信号可确保生成方等待缓冲区变空,使用者等待缓冲区变满为止。
typedef struct { char buf[BSIZE]; sem_t occupied; sem_t empty; int nextin; int nextout; sem_t pmut; sem_t cmut; } buffer_t; buffer_t buffer; sem_init(&buffer.occupied, 0, 0); sem_init(&buffer.empty,0, BSIZE); sem_init(&buffer.pmut, 0, 1); sem_init(&buffer.cmut, 0, 1); buffer.nextin = buffer.nextout = 0;
另一对二进制信号与互斥锁作用相同。在多个生成方使用多个空缓冲槽位,以及多个使用者使用多个满缓冲槽位的情况下,信号可用来控制对缓冲区的访问。在这种情况下,使用互斥锁可能会更好,但这里主要是为了演示信号的用法。
void producer(buffer_t *b, char item) { sem_wait(&b->empty); sem_wait(&b->pmut); b->buf[b->nextin] = item; b->nextin++; b->nextin %= BSIZE; sem_post(&b->pmut); sem_post(&b->occupied); }
char consumer(buffer_t *b) { char item; sem_wait(&b->occupied); sem_wait(&b->cmut); item = b->buf[b->nextout]; b->nextout++; b->nextout %= BSIZE; sem_post(&b->cmut); sem_post(&b->empty); return(item); }
通过读写锁,可以对受保护的共享资源进行并发读取和独占写入。读写锁是可以在读取或写入模式下锁定的单一实体。要修改资源,线程必须首先获取互斥写锁。必须释放所有读锁之后,才允许使用互斥写锁。
有关 Solaris 线程所实现的读写锁,请参见相似的同步函数-读写锁。
对数据库的访问可以使用读写锁进行同步。读写锁支持并发读取数据库记录,因为读操作不会更改记录的信息。要更新数据库时,写操作必须获取互斥写锁。
要更改缺省的读写锁属性,可以声明和初始化属性对象。通常,可以在应用程序开头的某个位置设置读写锁属性,设置在应用程序的起始位置可使属性更易于查找和修改。下表列出了本节中讨论的用来处理读写锁属性的函数。
表 4–8 读写锁属性例程
操作 |
相关函数说明 |
---|---|
初始化读写锁属性 |
pthread_rwlockattr_init 语法 |
销毁读写锁属性 |
pthread_rwlockattr_destroy 语法 |
设置读写锁属性 |
pthread_rwlockattr_setpshared 语法 |
获取读写锁属性 |
pthread_rwlockattr_getpshared 语法 |
pthread_rwlockattr_init(3C) 使用实现中定义的所有属性的缺省值来初始化读写锁属性对象 attr。
#includeint pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
如果调用 pthread_rwlockattr_init 来指定已初始化的读写锁属性对象,则结果是不确定的。读写锁属性对象初始化一个或多个读写锁之后,影响该对象的任何函数(包括销毁)不会影响先前已初始化的读写锁。
如果成功,pthread_rwlockattr_init() 会返回零。否则,将返回用于指明错误的错误号。
ENOMEM
描述:内存不足,无法初始化读写锁属性对象。
pthread_rwlockattr_destroy(3C) 可用来销毁读写锁属性对象。
#includeint pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
在再次调用 pthread_rwlockattr_init() 重新初始化该对象之前,使用该对象所产生的影响是不确定的。实现可以导致 pthread_rwlockattr_destroy() 将 attr 所引用的对象设置为无效值。
如果成功,pthread_rwlockattr_destroy() 会返回零。否则,将返回用于指明错误的错误号。
EINVAL
描述:attr 指定的值无效。
pthread_rwlockattr_setpshared(3C) 可用来设置由进程共享的读写锁属性。
#includeint pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
读写锁属性可以为以下值之一:
PTHREAD_PROCESS_SHARED
描述:允许可访问用于分配读写锁的内存的任何线程对读写锁进行处理。即使该锁是在由多个进程共享的内存中分配的,也允许对其进行处理。
PTHREAD_PROCESS_PRIVATE
描述:读写锁只能由某些线程处理,这些线程与初始化该锁的线程在同一进程中创建。如果不同进程的线程尝试对此类读写锁进行处理,则其行为是不确定的。由进程共享的属性的缺省值为 PTHREAD_PROCESS_PRIVATE。
如果成功,pthread_rwlockattr_setpshared() 会返回零。否则,将返回用于指明错误的错误号。
EINVAL
描述:attr 或 pshared 指定的值无效。
pthread_rwlockattr_getpshared(3C) 可用来获取由进程共享的读写锁属性。
#includeint pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
pthread_rwlockattr_getpshared() 从 attr 引用的已初始化属性对象中获取由进程共享的属性的值。
如果成功,pthread_rwlockattr_getpshared() 会返回零。否则,将返回用于指明错误的错误号。
EINVAL
描述:attr 或 pshared 指定的值无效。
配置读写锁的属性之后,即可初始化读写锁。以下函数用于初始化或销毁读写锁、锁定或解除锁定读写锁或尝试锁定读写锁。下表列出了本节中讨论的用来处理读写锁的函数。
表 4–9 处理读写锁的例程
操作 |
相关函数说明 |
---|---|
初始化读写锁 |
pthread_rwlock_init 语法 |
读取读写锁中的锁 |
pthread_rwlock_rdlock 语法 |
读取非阻塞读写锁中的锁 |
pthread_rwlock_tryrdlock 语法 |
写入读写锁中的锁 |
pthread_rwlock_wrlock 语法 |
写入非阻塞读写锁中的锁 |
pthread_rwlock_trywrlock 语法 |
解除锁定读写锁 |
pthread_rwlock_unlock 语法 |
销毁读写锁 |
pthread_rwlock_destroy 语法 |
使用 pthread_rwlock_init(3C) 可以通过 attr 所引用的属性初始化 rwlock 所引用的读写锁。
#includeint pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
如果 attr 为 NULL,则使用缺省的读写锁属性,其作用与传递缺省读写锁属性对象的地址相同。初始化读写锁之后,该锁可以使用任意次数,而无需重新初始化。成功初始化之后,读写锁的状态会变为已初始化和未锁定。如果调用 pthread_rwlock_init() 来指定已初始化的读写锁,则结果是不确定的。如果读写锁在使用之前未初始化,则结果是不确定的。对于 Solaris 线程,请参见rwlock_init 语法。
如果缺省的读写锁属性适用,则 PTHREAD_RWLOCK_INITIALIZER 宏可初始化以静态方式分配的读写锁,其作用与通过调用 pthread_rwlock_init() 并将参数 attr 指定为 NULL 进行动态初始化等效,区别在于不会执行错误检查。
如果成功,pthread_rwlock_init() 会返回零。否则,将返回用于指明错误的错误号。
如果 pthread_rwlock_init() 失败,将不会初始化 rwlock,并且 rwlock 的内容是不确定的。
EINVAL
描述:attr 或 rwlock 指定的值无效。
pthread_rwlock_rdlock(3C) 可用来向 rwlock 所引用的读写锁应用读锁。
#includeint pthread_rwlock_rdlock(pthread_rwlock_t *rwlock );
如果写入器未持有读锁,并且没有任何写入器基于该锁阻塞,则调用线程会获取读锁。如果写入器未持有读锁,但有多个写入器正在等待该锁时,调用线程是否能获取该锁是不确定的。如果某个写入器持有读锁,则调用线程无法获取该锁。如果调用线程未获取读锁,则它将阻塞。调用线程必须获取该锁之后,才能从 pthread_rwlock_rdlock() 返回。如果在进行调用时,调用线程持有 rwlock 中的写锁,则结果是不确定的。
为避免写入器资源匮乏,允许在多个实现中使写入器的优先级高于读取器。例如,Solaris 线程实现中写入器的优先级高于读取器。 请参见rw_rdlock 语法。
一个线程可以在 rwlock 中持有多个并发的读锁,该线程可以成功调用 pthread_rwlock_rdlock() n 次。该线程必须调用 pthread_rwlock_unlock() n 次才能执行匹配的解除锁定操作。
如果针对未初始化的读写锁调用 pthread_rwlock_rdlock(),则结果是不确定的。
线程信号处理程序可以处理传送给等待读写锁的线程的信号。从信号处理程序返回后,线程将继续等待读写锁以执行读取,就好像线程未中断一样。
如果成功,pthread_rwlock_rdlock() 会返回零。否则,将返回用于指明错误的错误号。
EINVAL
描述:attr 或 rwlock 指定的值无效。
pthread_rwlock_tryrdlock(3C) 应用读锁的方式与 pthread_rwlock_rdlock() 类似,区别在于如果任何线程持有 rwlock 中的写锁或者写入器基于 rwlock 阻塞,则 pthread_rwlock_tryrdlock() 函数会失败。对于 Solaris 线程,请参见rw_tryrdlock 语法。
#includeint pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
如果获取了用于在 rwlock 所引用的读写锁对象中执行读取的锁,则 pthread_rwlock_tryrdlock() 将返回零。如果没有获取该锁,则返回用于指明错误的错误号。
EBUSY
描述:无法获取读写锁以执行读取,因为写入器持有该锁或者基于该锁已阻塞。
pthread_rwlock_wrlock(3C) 可用来向 rwlock 所引用的读写锁应用写锁。
#includeint pthread_rwlock_wrlock(pthread_rwlock_t *rwlock );
如果没有其他读取器线程或写入器线程持有读写锁 rwlock,则调用线程将获取写锁。否则,调用线程将阻塞。调用线程必须获取该锁之后,才能从 pthread_rwlock_wrlock() 调用返回。如果在进行调用时,调用线程持有读写锁(读锁或写锁),则结果是不确定的。
为避免写入器资源匮乏,允许在多个实现中使写入器的优先级高于读取器。(例如,Solaris 线程实现允许写入器的优先级高于读取器。请参见rw_wrlock 语法。)
如果针对未初始化的读写锁调用 pthread_rwlock_wrlock(),则结果是不确定的。
线程信号处理程序可以处理传送给等待读写锁以执行写入的线程的信号。从信号处理程序返回后,线程将继续等待读写锁以执行写入,就好像线程未中断一样。
如果获取了用于在 rwlock 所引用的读写锁对象中执行写入的锁,则 pthread_rwlock_rwlock() 将返回零。如果没有获取该锁,则返回用于指明错误的错误号。
pthread_rwlock_trywrlock(3C) 应用写锁的方式与 pthread_rwlock_wrlock() 类似,区别在于如果任何线程当前持有用于读取和写入的 rwlock,则 pthread_rwlock_trywrlock() 函数会失败。对于 Solaris 线程,请参见 rw_trywrlock 语法。
#includeint pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
如果针对未初始化的读写锁调用 pthread_rwlock_trywrlock(),则结果是不确定的。
线程信号处理程序可以处理传送给等待读写锁以执行写入的线程的信号。从信号处理程序返回后,线程将继续等待读写锁以执行写入,就好像线程未中断一样。
如果获取了用于在 rwlock 引用的读写锁对象中执行写入的锁,则 pthread_rwlock_trywrlock() 将返回零。否则,将返回用于指明错误的错误号。
EBUSY
描述:无法为写入获取读写锁,因为已为读取或写入锁定该读写锁。
pthread_rwlock_unlock(3C) 可用来释放在 rwlock 引用的读写锁对象中持有的锁。
#includeint pthread_rwlock_unlock (pthread_rwlock_t *rwlock);
如果调用线程未持有读写锁 rwlock,则结果是不确定的。对于 Solaris 线程,请参见rw_unlock 语法。
如果通过调用 pthread_rwlock_unlock() 来释放读写锁对象中的读锁,并且其他读锁当前由该锁对象持有,则该对象会保持读取锁定状态。如果 pthread_rwlock_unlock() 释放了调用线程在该读写锁对象中的最后一个读锁,则调用线程不再是该对象的属主。如果 pthread_rwlock_unlock() 释放了该读写锁对象的最后一个读锁,则该读写锁对象将处于无属主、解除锁定状态。
如果通过调用 pthread_rwlock_unlock() 释放了该读写锁对象的最后一个写锁,则该读写锁对象将处于无属主、解除锁定状态。
如果 pthread_rwlock_unlock() 解除锁定该读写锁对象,并且多个线程正在等待获取该对象以执行写入,则通过调度策略可确定获取该对象以执行写入的线程。如果多个线程正在等待获取读写锁对象以执行读取,则通过调度策略可确定等待线程获取该对象以执行写入的顺序。如果多个线程基于 rwlock 中的读锁和写锁阻塞,则无法确定读取器和写入器谁先获得该锁。
如果针对未初始化的读写锁调用 pthread_rwlock_unlock(),则结果是不确定的。
如果成功,pthread_rwlock_unlock() 会返回零。否则,将返回用于指明错误的错误号。
pthread_rwlock_destroy(3C) 可用来销毁 rwlock 引用的读写锁对象并释放该锁使用的任何资源。
#includeint pthread_rwlock_destroy(pthread_rwlock_t *rwlock); pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
在再次调用 pthread_rwlock_init() 重新初始化该锁之前,使用该锁所产生的影响是不确定的。实现可能会导致 pthread_rwlock_destroy() 将 rwlock 所引用的对象设置为无效值。如果在任意线程持有 rwlock 时调用 pthread_rwlock_destroy(),则结果是不确定的。尝试销毁未初始化的读写锁会产生不确定的行为。已销毁的读写锁对象可以使用 pthread_rwlock_init() 来重新初始化。销毁读写锁对象之后,如果以其他方式引用该对象,则结果是不确定的。对于 Solaris 线程,请参见rwlock_destroy 语法。
如果成功,pthread_rwlock_destroy() 会返回零。否则,将返回用于指明错误的错误号。
EINVAL
描述:attr 或 rwlock 指定的值无效。
每个同步元语都可以跨进程边界使用。通过确保同步变量位于共享内存段中,并调用适当的 init() 例程,可设置元语。元语必须已经初始化,并且其共享属性设置为在进程间使用。
示例 4–17 说明了位于不同进程中的生成方和使用者的问题。主例程将与其子进程共享的全零内存段映射到其地址空间。
创建子进程是为了运行使用者,父进程则运行生成方。
本示例还说明了生成方和使用者的驱动程序。producer_driver() 可从 stdin 读取字符并调用 producer()。consumer_driver() 通过调用 consumer() 来获取字符并将这些字符写入 stdout 中。
示例 4–17 中的数据结构与示例 4–4 中所示用于条件变量示例的结构类似。两个信号分别空缓冲区和满缓冲区的数量,通过这些信号可确保生成方等待缓冲区变空,使用者等待缓冲区变满为止。
main() { int zfd; buffer_t *buffer; pthread_mutexattr_t mattr; pthread_condattr_t cvattr_less, cvattr_more; zfd = open("/dev/zero", O_RDWR); buffer = (buffer_t *)mmap(NULL, sizeof(buffer_t), PROT_READ|PROT_WRITE, MAP_SHARED, zfd, 0); buffer->occupied = buffer->nextin = buffer->nextout = 0; pthread_mutex_attr_init(&mattr); pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(&buffer->lock, &mattr); pthread_condattr_init(&cvattr_less); pthread_condattr_setpshared(&cvattr_less, PTHREAD_PROCESS_SHARED); pthread_cond_init(&buffer->less, &cvattr_less); pthread_condattr_init(&cvattr_more); pthread_condattr_setpshared(&cvattr_more, PTHREAD_PROCESS_SHARED); pthread_cond_init(&buffer->more, &cvattr_more); if (fork() == 0) consumer_driver(buffer); else producer_driver(buffer); } void producer_driver(buffer_t *b) { int item; while (1) { item = getchar(); if (item == EOF) { producer(b, `/0'); break; } else producer(b, (char)item); } } void consumer_driver(buffer_t *b) { char item; while (1) { if ((item = consumer(b)) == '/0') break; putchar(item); } }
线程中最基本的同步元语是互斥锁。因此,在内存使用和执行时间这两个方面,互斥锁都是最高效的机制。互斥锁的基本用途是按顺序访问资源。
线程中第二高效的元语是条件变量。条件变量的基本用途是基于状态的变化进行阻塞。条件变量可提供线程等待功能。请注意,线程在基于条件变量阻塞之前必须首先获取互斥锁,在从 pthread_cond_wait() 返回之后必须解除锁定互斥锁。线程还必须在状态发生改变期间持有互斥锁,然后才能对 pthread_cond_signal() 进行相应的调用。
信号比条件变量占用更多内存。由于信号变量基于状态而非控制来工作,因此在某些情况下更易于使用。与锁不同,信号没有属主。任何线程都可以增加已阻塞的信号。
通过读写锁,可以对受保护的资源进行并发读取和独占写入。读写锁是可以在读取或写入模式下锁定的单一实体。要修改资源,线程必须首先获取互斥写锁。必须释放所有读锁之后,才允许使用互斥写锁。