2024年2月26日发(作者:)
Unix_Linux_Windows_OpenMP多线程编程
第三章 Unix/Linux 多线程编程[引言]本章在前面章节多线程编程基础知识的基础上,着重介绍 Unix/Linux 系统下的多线
程编程接口及编程技术。
3.1 POSIX 的一些基本知识
POSIX 是可移植操作系统接口(Portable Operating System
Interface)的首字母缩写。
POSIX 是基于 UNIX 的,这一标准意在期望获得源代码级的软件可移植性。换句话说,为一
个 POSIX 兼容的操作系统编写的程序,应该可以在任何其它的 POSIX 操作系统(即使是来自
另一个厂商)上编译执行。POSIX 标准定义了操作系统应该为应用程序提供的接口:系统调
用集。POSIX是由 IEEE(Institute of Electrical and
Electronic Engineering)开发的,
并由 ANSI(American National Standards Institute)和 ISO(International
Standards
Organization)标准化。大多数的操作系统(包括 Windows NT)都倾向于开发它们的变体
版本与 POSIX 兼容。
POSIX 现在已经发展成为一个非常庞大的标准族,某些部分正处在开发过程中。表 1-1 给
出了 POSIX 标准的几个重要组成部分。POSIX 与 IEEE 1003 和 2003 家族的标准是可互换
的。除 1003.1 之外,1003 和 2003 家族也包括在表中。
管理 POSIX 开放式系统环境(OSE) 。IEEE 在 1995 年通过了这项标准。 ISO
的
1003.0
版本是 ISO/IEC 14252:1996。
被广泛接受、用于源代码级别的可移植性标准。1003.1 提供一个操作系统的
C 语
1003.1 言应用编程接口(API) 。IEEE 和 ISO 已经在 1990 年通过了这个标准,IEEE 在
1995 年重新修订了该标准。
一个用于实时编程的标准(以前的 P1003.4 或 POSIX.4)。这个标准在 1993
年被
1003.1b
IEEE 通过,被合并进 ISO/IEC 9945-1。
一个用于线程(在一个程序中当前被执行的代码段)的标准。以前是 P1993.4
或
1003.1c POSIX.4 的一部分,这个标准已经在 1995 年被 IEEE
通过,归入 ISO/IEC
9945-1:1996。
一个关于协议独立接口的标准,该接口可以使一个应用程序通过网络与另一个应用
1003.1g
程序通讯。 1996 年,IEEE 通过了这个标准。
一个应用于 shell 和工具软件的标准,它们分别是操作系统所必须提供的命令处
1003.2 理器和工具程序。 1992 年 IEEE 通过了这个标准。ISO 也已经通过了这个标准
(ISO/IEC 9945-2:1993) 。 1003.2d 改进的 1003.2 标准。
一个相当于 1003.1 的 Ada 语言的 API。在 1992 年,IEEE 通过了这个标准。并
1003.5
在 1997 年对其进行了修订。ISO 也通过了该标准。
一个相当于 1003.1b(实时扩展)的 Ada 语言的 API。IEEE 和 ISO 都已经通过
1003.5b
了这个标准。ISO 的标准是 ISO/IEC 14519:1999。
一个相当于 1003.1q(协议独立接口)的 Ada 语言的 API。在 1998 年, IEEE
通
1003.5c
过了这个标准。ISO 也通过了这个标准。
一个相当于 1003.1 的 FORTRAN 语言的 API。在 1992 年,IEEE 通过了这个标准,
1003.9
并于 1997 年对其再次确认。ISO 也已经通过了这个标准。
一个应用于超级计算应用环境框架(Application Environment
Profile,AEP)的
1003.10
标准。在 1995 年,IEEE 通过了这个标准。
一个关于应用环境框架的标准,主要针对使用 POSIX 接口的实时应用程序。在
1003.13
1998 年,IEEE 通过了这个标准。
1003.22 一个针对 POSIX 的关于安全性框架的指南。
一个针对用户组织的指南,主要是为了指导用户开发和使用支持操作需求的开放式
1003.23
系统环境(OSE)框架
针对指定和使用是否符合 POSIX 标准的测试方法,有关其定义、一般需求和指导
2003
方针的一个标准。在 1997 年,IEEE 通过了这个标准。
这个标准规定了针对 1003.1 的 POSIX 测试方法的提供商要提供的一些条件。在
2003.1
1992 年,IEEE 通过了这个标准。
一个定义了被用来检查与 IEEE 1003.2(shell 和 工具 API)是否符合的测试方
2003.2
法的标准。在 1996 年,IEEE 通过了这个标准。
表 3.1 POSIX 标准的重要组成部分
本章将重点讲述“POSIX线程”,即符合 POSIX 国际正式标准 POSIXl003.1c-1995 的部分。
本章假定用户使用的编程语言为 ANSI C 语言。
3.2 POSIX 线程库
首先,在编写 POSIX 多线程 C 程序时,需要包含头文件’pthread.h’。POSIX
线程函数都
以’pthread_’开头。在本章中,我们将介绍一下线程操作函数:
POSIX 函数 描述
pthread_cancel 终止另一个线程pthread_create 创建一个线程
pthread_detach 设置线程以释放资源
pthread_equal 测试两个线程 ID 是否相等pthread_exit 退出线程,而不退出进程2 pthread_join 等待一个线程
pthread_self 找出自己的线程 ID
表 3.2 POSIX 线程管理函数
3.2.1 创建线程
‘pthread_create’ 函数创建一个线程。int pthread_createpthread_t
*restrict thread, const
pthread_attr_t* restrict attr, void **start_routinevoid *,
void *restrict arg;
参数 thread 指向保存线程 ID 的 pthread_t 结构。参数 attr 表示一个封装了线程的各种属
性的属性对象,用来配置线程的运行,如果为 NULL,则使新线程具有默认的属性。线程属
性将在后面的 XX 节讨论。第三个参数 start_routine 是线程开始执行的时候调用的函数的
名字。这个函数必须具有以下的格式:
void* start_routinevoid* arg;
返回的 void指针将被 pthread_join 函数当做退出状态来处理。第四个参数
arg 正是传递给
start_routine 函数的参数。POSIX 的pthread_create 函数会使创建的线程自动处于可运行
状态,而不需要一个单独的启动操作。
如果成功,pthread_create 返回 0,如果不成功,pthread_create 返回一个非零的错误码。
下表列出了 pthread_create 的错误形式及相应的错误码
错误 原因EAGAIN 系统没有创建线程所需的资源,或者创建线
程会超出系统对一个进程中线程总数的限制
EINVAL attr 参数是无效的
EPERM 调用程序没有适当的权限来设定调度策略或
attr 指定的参数
表 3.3 pthread_create的错误形式及相应的错误码
每一个线程可以通过调用函数 pthread_self 得到本线程的 ID(数据结构类型:pthread_t) ,
它的形式为:
pthread_t pthread_selfvoid;
由于 pthread_t 可能是一个结构,因此 POSIX 提供了一个函数
pthread_equal 来比较线程
ID 是否相等。这个函数的形式为:
int pthread_equalpthread_t t1, pthread_t t2;3
两个参数 t1和 t2 是两个线程 ID,如果它们相等,pthread_equal 就返回一个非零值,如果
不相等,则返回 0。
3.2.2 分离(Detach)和接合(Join)线程
POSIX 线程的一个特点是:除非线程是被分离了的,否则在线程退出时,它的资源是不会被
释放的。pthread_detach函数用来分离线程:
int pthread_detachpthread_t thread;
它设置线程的内部选项来说明线程退出后,其所占有的资源可以被回收。参数
thread 是要
分离的线程的 ID。被分离的的线程退出时不会报告它们的状态。如果函数调用成功,
pthread_detach 返回 0,如果不成功,pthread_detach 返回一个非零的错误码。下表列出
了 pthread_detach 的错误形式及相应的错误码
错误 原因
EINVAL thread 对应的不是一个可分离的线程ESRCH 没有 ID 为 thread 的线程
表 3.4 ‘pthread_detach’的错误形式及相应的错误码
pthread_join 函数可以使调用这个函数的线程等待指定的线程运行完成再继续执行。它的
形式为:
int pthread_joinpthread_t thread, void **value_ptr;
参数 thread为要等待的线程的 ID,参数 value_ptr 为指向返回值的指针提供一个位置,这
个返回值是由目标线程传递给 pthread_exit 或 return 的。如果 value_ptr
为 NULL,调用
程序就不会对目标线程的返回状态进行检索了。如果函数调用成功,pthread_join返回 0,
如果不成功,pthread_join 返回一个非零的错误码。下表列出了
pthread_join 的错误形式
及相应的错误码
错误 原因EINVAL thread 对应的不是一个可接合的线程
ESRCH 没有 ID 为 thread 的线程
表 3.5 pthread_join 的错误形式及相应的错误码
如果线程没有被分离,并且执行 pthread_joinpthread_self,那么该线程将被一直挂
起,因为这条语句造成了死锁。有些 POSIX 的实现可以检测到死锁,并迫使
pthread_join
带着错误 EDEADLK 返回,但是,POSIX并不要求一定要进行这种检测。4 3.2.3
退出和取消线程
进程的终止可以通过在主函数 main中直接调用 exit、return、或者通过进程中的任何其
它线程调用 exit 来实现。在任何一种情况下,该进程的所有线程都会终止。如果主线程在
创建了其它线程之后没有工作可做,它就应该阻塞到所有线程都结束为止,或者应该调用
pthread_exitNULL。
有时程序不必等待线程执行完成,这时程序需要使线程中途退出。POSIX 线程库提供了两个
撤销线程的函数 pthread_exit 和pthread_cancel。下面对这
两个函数分别进行介绍。
pthread_exit 函数可以使调用这个函数的线程中止运行,并且允许线程传递一个指针,这
个指针可以用来指向线程的返回值。它的形式为:
void pthread_exitvoid *value_ptr;
连接了这个线程可以获得参数 value_ptr 的值。回顾前面介绍的
pthread_join 函数,这个
函数的参数 void **value_ptr,正是保存 pthread_exit 函数的参数 void
*value_ptr 的地
址。这里要注意,pthread_exit 的参数 value_ptr 必须指向线程退出后仍然存在的数据。
POSIX 没有为 pthread_exit 定义任何错误。
POSIX doesn't define any error code for ‘pthread_exit’
线程也可以通过取消机制迫使其它的线程退出。线程可以调用函数
pthread_cancel 来请求
取消另一个线程。这个函数的形式是:
int pthread_cancelpthread_t thread;
参数 thread是要取消的目标线程的线程 ID。该函数并不阻塞调用线程,它发出取消请求后
就返回了。如果成功,pthread_cancel 返回 0,如果不成功,pthread_cancel
返回一个非
零的错误码。
线程收到一个取消请求时会发生什么情况取决于它的状态和类型。如果线程处于
PTHREAD_CANCEL_ENABLE状态,它就接受取消请求,如果线程处于
PTHREAD_CANCEL_DISABLE
状态,取消请求就会被保持在挂起状态。默认情况下,线程处于
PTHREAD_CANCEL_ENABLE
状态。
pthread_setcancelstate函数用来改变调用线程的取消状态,它的形式为:
int pthread_setcancelstateint state, int *oldstate;
参数 state 表示要设置的新状态,参数 oldstate 为一个指向整形的指针,用于保存线程以
前的状态。如果成功,该函数返回 0,如果不成功,它返回一个非 0 的错误码。通常情况下,
线程函数在改变了线程的取消状态之后,应该在执行完某些操作之后恢复线程的取消状态,
5否则,对于其它可能取消该线程的线程而言,取消操作的结果将无法预测,这很可能不利于
程序的正确执行。
当线程将退出作为对取消请求的响应时,取消类型允许线程控制它在什么地方退出。当它的
取消类型为 PTHREAD_CANCEL_ASYNCHRONOUS 时,线程在任何时
候都可以响应取消请求。当它
的取消类型为 PTHREAD_CANCEL_DEFERRED 时,线程只能在特定的几个取消点上响应取消请
求。在默认情况下,线程的类型为 PTHREAD_CANCEL_DEFERRED。
pthread_setcanceltype函数用来修改线程的取消类型。它的形式为:
int pthread_setcanceltypeint type, int *oldtype;
参数 type 指定线程的取消类型,参数 oldtype 用来指定保存原来的取消类型的地址。如果
成功,该函数返回 0,如果不成功,它返回一个非 0 的错误码。
线程可以通过调用 pthread_testcancel 在代码中的特定的位置上设置一个取消点。当类型
为 PTHREAD_CANCEL_DEFERRED 的线程到达这样一个取消点时,就接受挂起的取消请求。该函
数的形式为:
void pthread_testcancelvoid;
3.2.4 用户级线程与内核级线程
用户级线程user-level thread和内核级线程kernel-level
thread是两种传统的线程控
制模式。用户级线程通常都运行在一个现存的操作系统之上。这些线程对内核来说是不可见
的,它们被封装在进程里,并竞争分配给进程的资源。线程由一个
线程运行系统来调度,这
个系统是进程代码的一部分。带有用户级线程的程序通常会连接到一个特殊的库上去,这个
库中的每个库函数都用外套jacket包装起来。在调用被外套包装的库函数之前,外套函数
要调用线程运行系统来进行线程管理,在调用了被外套包装的库函数之后,外套函数可能也
要进行这样的操作。
这样做的必要性是为了解决下面的情况:由于 read或 sleep这样的函数可能会使进程阻塞,
所以它们给用户级线程带来了一个问题,那就是要避免某个线程在调用这些阻塞型函数之
后,整个进程被阻塞。这就要求用户级线程库用一个无阻塞的版本来替换每一个外套包装的、
潜在的阻塞型调用。线程运行系统通过测试来查看调用是否会使线程阻塞,如果调用不会阻
塞,运行系统就立即进行调用,但是,如果调用会阻塞,运行系统就会将线程放在一个等待
线程的列表中,将调用添加到一个动作列表中,以便稍后再试,然后挑选另一个线程来运行。
所有这些控制过程对用户和操作系统来说都是不可见的。
用户级线程的开销很低,但是它们也有些缺点。用户线程模型假
定线程运行系统昀终会重新
获得控制权,这可能会受到 CPU 绑定线程CPU-bound thread的阻碍。CPU 绑定线程很少执
行库函数调用,这样就会阻止线程运行系统重新获得控制权来调度其它的线程。程序员必须
要显式地迫使 CPU 绑定线程在适当的地方放弃对 CPU 的控制,以避免出现封锁状态。第二个
问题是,用户级线程只能共享分配给它们的封装进程的处理器资源。因为线程一次只能运行
6 在一个处理器上,这种约束限制了可用的并行总量。使用线程的主要原因之一就是要利用多
处理器工作站的优势,所以仅使用用户级线程本身并不是一种能让人接受的方法。
对内核级线程来说,内核了解每一个作为可调度实体的线程,这些线程可以在全系统范围内
竞争处理器资源。内核级线程的调度开销可能和进程自身的调度差不多昂贵,但是,内核级
线程可以利用多处理器的优势。内核级线程的同步和数据共享比整个进程的同步和数据共享
的开销要低一些,但内核级线程的管理比用户级线程的管理代价更高。
还有一种模型,称作混合线程模型hybrid thread model,它通过
提供两个级别的控制,
同时具备了用户级和内核级模型的优点。用户用用户级线程编写程序,然后说明有多少个内
核可调度实体与这个进程相关。运行时,将用户级线程映射为系统的可调度实体,以实现并
行。用户拥有的映射控制级别取决于实现,例如在 Sun 的Solaris 线程实现中,用户级线程
被称为线程,而内核可调度实体被称为轻量级进程lightweight
process。用户可以指定
由一个特定的轻量级进程来运行制定的线程,或者由一个轻量级进程池来运行一组制定的线
程。
POSIX 线程调度模型是一个混合模型,它很灵活,足以在标准的特定实现中支持用户级和内
核级的线程。模型中包括两级调度??线程级和内核实体级。线程与用户级线程类似,内核
实体由内核调度。由线程库来决定它需要多少内核实体,以及它们是如何映射的。
POSIX 引入了一个线程调度竞争范围thread-scheduling
contention scope的概念,这个
概念赋予了程序员一些控制权,使它们可以控制怎样将内核实体映射为线程。线程的
contentionscope 属性可以是 PTHREAD_SCOPE_PROCESS,也可以是
PTHREAD_SCOPE_SYSTEM。
带有 PTHREAD_SCOPE_PROCESS 属性的线程与它们所在的进程中的其它线程竞争处理器资源。
POSIX 没有说明这样一个线程怎样与它所在的进程中的其它线程竞争,因此
PTHREAD_SCOPE_PROCESS线程可以是严格的用户级线程,或者它们也可以使用某种更复杂的
方式映射到一个内核实体池中去。带有 PTHREAD_SCOPE_SYSTEM
属性的线程很像内核级线程,
他们在全系统范围内竞争处理器资源。POSIX 将 PTHREAD_SCOPE_SYSTEM 线程和内核实体之
间的映射留给具体实现来完成,但是一种明显的映射方式是,将这样一个线程直接与内核实
体绑定起来。 POSIX 线程的具体实现可能支持 PTHREAD_SCOPE_PROCESS、或
PTHREAD_SCOPE_SYSTEM 或者两者都支持。
3.2.5 线程的属性
POSIX 将栈的大小和调度策略这样的特征封装到一个 pthread_attr_t 类型的对象中去,用
面向对象的方式表示和设置特征。属性对象只在线程创建的时候会对线程产生影响。编写程
序时可以先创建一个属性对象,然后再将栈的大小和调度策略这样的特征与属性对象关联起
来,之后就可以通过向 pthread_create 传递相同的线程属性对象来创建多个具有相同特征
的线程。通过将各种特征组合到单个对象中去,POSIX 避免了用大量参数来调用
pthread_create 的情况。
表 3.6 显示的是线程属性的可设置特征及其相关函数,后面我们将对这些特征和函数进行讨
论。
7特征 函数
属性对象 pthread_attr_destroy
pthread_attr_init
状态 pthread_attr_getdetachstatepthread_attr_setdetachstate
栈 pthread_attr_getguardsizepthread_attr_setguardsize
pthread_attr_getstack
pthread_attr_setstack
调度 pthread_attr_getinheritschedpthread_attr_setinheritsched
pthread_attr_getschedparam
pthread_attr_setschedparam
pthread_attr_getschedpolicy
pthread_attr_setschedpolicy
pthread_attr_getscope
pthread_attr_setscope
表 3.6 线程属性的可设置特征及其相关函数
函数 pthread_attr_init用默认值对一个线程属性对象进行初始化。pthread_attr_destroy
函数将属性对象的值设为无效的。被设为无效的属性对象可以再次被初始化为一个新的属性
对象。pthread_attr_init 和 pthread_attr_destroy 都只有一个参数,即一个指向属性对
象的指针。这两个函数的形式为:
int pthread_attr_initpthread_attr_t *attr;
int pthread_attr_destroypthread_attr_t *attr;
如果成功,函数返回 0,如果不成功,函数返回一个非 0 的错误码。
大多数针对属性对象的函数都是获取或设置属性对象的属性。第一个参数是一个指向属性对
象的指针。对于获取操作,第二个参数是一个指向存放值的位置的指针,而对于设置操作,
第二个参数是属性的设置值。因此,后面读者可以根据函数参数的名称和类型推断出参数的
含义,我们就不一一介绍了。
3.2.5.1 线程状态线程状态的可能取值为 PTHREAD_CREATE_JOINABLE 和
PTHREAD_CREATE_DETACHED。
pthread_attr_getdetachstate 函 数 用 来 查 看 一 个 属 性 对 象 中
的 线 程 状 态 , 而
pthread_attr_setdetachstate 函数用来设置一个属性对象中的线程状态。这两个函数的形
式为:
int pthread_attr_getdetachstateconst pthread_attr_t
*attr, int *detachstate;
int pthread_attr_setdetachstatepthread_attr_t *attr, int
detachstate;
8 如果成功,这些函数都返回 0。如果不成功,他们就返回一个非零的错误码。
如前所述,可以通过调用 pthread_detach 函数来分离一个线程,现在也可以通过先设置属
性对象的线程状态为 PTHREAD_CREATE_DETACHED,并在创建线程时传递这个属性对象,使线
程处于分离状态。被分离的线程是不能用 pthread_join 来等待的。默认情况下,线程是可
接合的。
3.2.5.2 线程栈
线程有自己的栈,用户可以设置这个栈的位置和大小,这就必须先用特定的栈属性来创建一
个属性对象,然后把这个属性对象传递给 pthread_create 来创建线程。
pthread_attr_getstack 函数用来查看栈的参
数,pthread_attr_setstack 函数用来设置一
个属性对象的栈参数。这两个函数的形式为:
int pthread_attr_getstackconst pthread_attr_t *restrict
attr, void **restrict
stackaddr, size_t *restrict stacksize;
int pthread_attr_setstackpthread_attr_t *attr, void
*stackaddr, size_t
stacksize;
如果成功,这些函数都返回 0。如果不成功,他们就返回一个非零的错误码。如果 stacksize
超出了范围,pthread_attr_setstack 函数就将errno 设置为EINVAL。
如果没有为线程指定堆栈,用户可以调用 POSIX提供的检查栈溢出或者为栈溢出设置警戒的
函数。pthread_attr_getguardsize 函数用来查看警戒参数,pthread_attr_setguardsize
函数在一个属性对象中设置了用来控制栈溢出的警戒参数。如果
参数 guardsize为0,栈就
是无警戒的,如果非 0,那么线程栈将至少多获得 guardsize 的额外内存。对这个额外内存
区的溢出会引发一个错误。这两个函数的形式为:
int pthread_attr_getguardsizeconst pthread_attr_t *
restrict attr, size_t
*restrict guardsize;
int pthread_attr_setguardsizepthread_attr_t *attr,
size_t guardsize;
如果成功,这些函数都返回 0。如果不成功,他们就返回一个非零的错误码。如果参数 attr
或 guardsize 是无效的,他们就返回 EINVAL。
POSIX 规定线程的栈如果被应用程序自己定义的话,栈的空间管理大小确定,空间伸缩等
需要应用程序自己管理,比如应用程序定义了自己的栈大小是 10M,应用程序必须确定这 10M
空间够大,而且在空间不够的情况下,应用程序需要自己扩展栈的大小,否则的话,可能会
发生栈溢出的错误。如果应用程序不自定义栈的话,POSIX的线程实现机制会保证应用程序
的线程栈按照运行情况自动调整。除了主线程以外,其他的线程的栈的管理都是在堆里实现
的。原则上不建议应用程序管理线程栈,除非应用程序在空间上
需要精心设计。93.2.5.3 线程调度
线程调度的竞争范围控制了线程是在进程内部还是在系统级竞
争调度资源。
pthread_attr_getscope 用来查看竞争范
围,pthread_attr_setscope 用来设置一个属性对
象的竞争范围。这两个函数的形式为:
int pthread_attr_getscopeconst pthread_attr_t *restrict
attr, int *restrict
contentionscope;
int pthread_attr_setscopepthread_attr_t *attr, int
contentionscope;
参数 contentionscope 的可能取值为 PTHREAD_SCOPE_PROCESS 和
PTHREAD_SCOPE_SYSTEM。
如果成功,这些函数都返回 0。如果不成功,他们就返回一个非零
的错误码。
POSIX 允许线程用不同的方式继承调度策略。
pthread_attr_getinheritsched 函数用于查看
调度继承策略,而 pthread_attr_setinheritsched用于为一个
属性对象设置调度继承策略。
这两个函数的形式为:
int pthread_attr_getinheritschedconst pthread_attr_t
*restrict attr, int
*restrict inheritsched;
int pthread_attr_setinheritschedpthread_attr_t *attr,
int inheritsched;
inheritsched 有两个可能的取值:PTHREAD_INHERIT_SCHED 和
PTHREAD_EXPLICIT_SCHED。
使用 PTHREAD_INHERIT_SCHED 时,调度属性从创建线程中继承,传递的属性对象中的调度属
性将被忽略。使用 PTHREAD_EXPLICIT_SCHED 时,线程使用属性对象中的调度属性。如果成
功,这些函数都返回 0。如果不成功,他们就返回一个非零的错误码。
pthread_attr_getschedpolicy 函数负责获取调度策略,pthread_attr_setschedpolicy 函
数负责设置属性对象的调度策略。这两个函数的形式为:
int pthread_attr_getschedpolicyconst pthread_attr_t
*restrict attr, int *restrict
policy;
int pthread_attr_setschedpolicypthread_attr_t *attr, int
policy;
参数 policy为调度策略。头文件 sched.h 为先进先出调度策略定义了
SCHED_FIFO,为轮转
调度定义了 SCHED_RR,并为一些其他的策略定义了 SCHED_OTHER。实现POSIX
线程库的操作
系统也可以有自己的调度策略。如果成功,这些函数都返回 0。如果不成功,他们就返回一
个非零的错误码。
pthread_attr_getschedparam 函数负责查看调度参数,而
pthread_attr_setschedparam负
责设置一个属性对象的调度参数。这两个函数的形式为:
int pthread_attr_getschedparamconst pthread_attr_t
*restrict attr, struct
sched_param *restrict param;
int pthread_attr_setschedparampthread_attr_t *restrict
attr, const struct10 sched_param *restrict param;
如果成功,这些函数都返回 0。如果不成功,他们就返回一个非零的错误码。
参数 struct sched_param 是一个定义在 sched.h 头文件中的结构,它为特定的调度策略服
务。SCHED_FIFO 和 SCHED_RR 调度策略只使用这个结构中的 sched_priority
成员。
sched_priority 成员为一个 int 型的优先级值,较大的优先级值对应于较高的优先级。实
现 POSIX 线程库的操作系统必须至少支持 32 个优先级。
3.2.6 线程安全函数线程中隐藏的一个问题是它们可能会调用非线程安全的库函数,这样可能会产生错误的结
果。如果多个线程能够同时执行函数的多个活动请求而不会相互干扰,那么这个函数就是线
程安全的thread-safe。POSIX 规定,除了表 3.7 列出的特定的函数之外,所有必需的函
数,包括来自标准 C 库中的函数,都要用线程安全的方式来实现。有些函数的传统接口会妨
碍它们成为线程安全的函数,这些函数一定要有一个以后缀_r 表示对应的线程安全版本。
有些函数不一定非要是线程安全的,strerror 就是这种函数的一个很重要的例子。尽管不
能保证 strerror 是线程安全的,但是很多系统都用线程安全的模式来实现这个函数。不幸
的是,由于 strerror 被列在表 3.7 中,所以如果有多个线程调用它的话,你就不能假设它
能正确地工作。一般我们只在主线程中使用 strerror,通常为
pthread_create 和
pthread_join 产生的错误消息。而各个子线程也可能产生自己的错误,这时调用 strerror
就可能产生线程不安全问题,因为在传统的 UNIX 实现中,errno 是一个全局外部变量,当
系统函数产生一个错误时就会设置 errno。对多线程来说,这种
实现方式是无法工作的,因
为每个线程都应该有自己的 errno。有些系统实现了线程安全的
strerror_r 版本,而对于
那些没有实现 strerror_r 的系统,也可以自己编写线程安全的
strerror_r 函数。
asctime fcvt getpwnam nl-langinfo
basename ftw getpwuid ptsname
catgets gcvt getservbyname putc_unlocked crypt getc_unlocked
getservbyport putchar_unlocked ctime getchar-unlocked getservent putenv
dbm_clearerr getdate getutxent pututxline dbm_close getenv getutxid rand
dbm_delete getgrent getutxline readdir dbm_error getgrid gmtime
setenv
dbm_fetch getgrnam hcreate setgrent dbm_firstkey gethostbyaddr
hdestroy setkey dbm_nextkey gethostbyname hsearch setpwent dbm_open
gethostent inet_ntoa setutxent dbm_store getlogin l64a strerror
dirname getnetbyaddr lgamma strtok dlerror getnetbyname lgammaf
ttyname11drand48 getnetent
lgammal unsetenv
ecvt getopt localeconv wcstombs
encrypt getprotobyname localtime wctomb
endgrent getprotobynumber lrand48endpwent getprotoent
mrand48endutxent getpwent nftw表 3.7 POSIX规定的非线程安全的函数
表 3.7 列出了 POSIX 规定的非线程安全的函数,除了该表中的函数,其它的函数必须要用
线程安全的方式实现。
虽然表 3.7中的函数不是必须线程安全的,但某些系统仍然实现了其中一些函数的线程安全
版本,比如 asctime 函数对应的线程安全版本 asctime_r,getgrid 函数对应的线程安全版
本 getgrid_r 等。
3.2.7 线程特定数据
在单线程程序中,函数经常使用全局变量或静态变量,这是不会影响程序的正确性的,但如
果线程调用的函数使用全局变量或静态变量,则很可能引起编程错误,因为这些函数使用的
全局变量和静态变量无法为不同的线程保存各自的值,而当同一进程内的不同线程几乎同时
调用这样的函数时就可能会有问题发生。而解决这一问题的一种
方式就是使用线程特定数据
的机制。
下面我们引入一个简单程序实例,并以此作为介绍线程特定数据的案例。
代码3. 1 线程特定数据
static char str[100];void Achar* s strncpystr, s, 100;void
B printf“%sn”, str;
可以想象,如果在多线程程序中,各个线程都依次调用函数 A 和函数B,那么某些线程可能
得不到期望的显示结果,因为它使用 B 显示的字符串可能并不是在 A 中设置的字符串。读者
会发现,这两个函数非常的简单,但在本章内容中,这两个函数已经足以解释线程特定数据
的含义,因为这两个函数代表了使用线程特定数据机制的一种典型场合,即有多个函数使用
同一个全局变量。
POSIX 要求实现 POSIX 的系统为每个进程维护一个称之为 Key 的结构数组,这个数组中的每
12 一个结构称之为一个线程特定数据元素。POSIX规定系统实现的 Key 结构数组必须包含不少
于 128 个线程特定数据元素,而每个线程特定数据元素中至少包含两项内容:使用标志和析
构函数指针。线程特定数据元素中的使用标志指示这个数组元素是否正在使用,初始值为“不
在使用”, 我们稍后讨论线程特定数据元素中的析构函数指针。在后面的介绍中,我们假设
Key 结构数组中包含 128个元素。
Key 结构数组中每个元素的索引(0~127)称之为键(key),当一个线程调用
pthread_key_create 创建一个新的线程特定数据元素时,系统搜索其所在进程的 Key 结构
数组,找出其中第一个不在使用的元素,并返回该元素的键。这个函数的形式为:
int pthread_key_createpthread_key_t *keyptr, void *
destructorvoid *value;
参数 keyptr 为一个 pthread_key_t 变量的指针,用于保存得到的键值。参数
destructor
为指定的析构函数的指针。
除了 Key 结构数组,系统还在进程中维护关于每个线程的多种信息。这些特定于线程的信息
被保存于称之为 Pthread 的结构中。Pthread 结构中包含名为 pkey 的指针数组,其长度为
128,初始值为空。这 128 个指针与 Key 结构数组的 128 个线程特定数据元素一一对应。在
调用 pthread_key_create得到一个键之后,每个线程可以依据这个键操作自己的 pkey指针
数组中对应的指针,这通过 pthread_getspecific和 pthread_setspecific函数来实现。这
两个函数的形式为:
void *pthread_getspecificpthread_key_t key;
int pthread_setspecificpthread_key_t key, const void
*value;
pthread_getspecific 返回 pkey 中对应于 key 的指针,而
pthread_setspecific 将pkey 中
对应于 key的指针设置为 value。
我们使用线程特定数据机制,就是要使线程中的函数可以共享一些数据。如果我们在线程中
通过 malloc 获得一块内存,并把这块内存的指针通过 pthread_setspecific
设置到 pkey
指针数组中对应于 key的位置,那么线程中调用的函数即可通过
pthread_getspecific 获得
这个指针,这就实现了线程内部数据在各个函数间的共享。当一个线程终止时,系统将扫描
该线程的 pkey 数组,为每个非空的 pkey 指针调用相应的析构函数,因此只要将执行回收的
函数的指针在调用 pthread_key_create 时作为函数的参数,即
可在线程终止时自动回收分
配的内存区。
下面我们可以通过实例来理解线程特定数据的机制:
代码3. 2 线程特定数据的机制
#include ‘stdio.h’
#include ‘pthread.h’
#include ‘string.h’
13#define LEN 100
pthread_key_t key;
void Achar *s
char *str char*pthread_getspecifickey;strncpystr, s,
LEN;
void B
char *str char*pthread_getspecifickeyprintf"%sn", str; void
destructorvoid *ptr
freeptr;printf"memory freedn";
void *threadfunc1void *pvoid
pthread_setspecifickey, mallocLEN;A"Thread1";B; void
*threadfunc2void *pvoid
pthread_setspecifickey, mallocLEN;A"Thread2";B; int main
pthread_t tid1, tid2;pthread_key_create&key,
destructor;pthread_create&tid1, NULL, &threadfunc1,
NULL;pthread_create&tid2, NULL, &threadfunc2,
NULL;14
pthread_exitNULL;return 0;
在这个程序中,函数 A 和函数 B 共享了一个内存区,而这个内
存区是特定于调用 A 和B 的线
程的,对于其它线程,这个内存区是不可见的。这就安全有效地达
到了在线程中的各个函数
之间共享数据的目的。
3.2.8 一个POSIX 多线程实例
代码3.3一个 POSIX多线程实例
#include ‘stdio.h’
#include ‘pthread.h’
void *threadfuncvoid *pvoid
int id intpvoid;printf"Child thread%d
says:Hello!World!n", id;
return NULL;
int main
pthread_t tid1, tid2;pthread_create&tid1, NULL, &threadfunc,
1;pthread_create&tid2, NULL, &threadfunc,
2;
pthread_detachtid1;pthread_jointid2, NULL;printf"Main
thread
says:Hello!World!n";
return 0;
这是一个十分简单的 POSIX 多线程程序。主线程创建两个子线程,并分别将 1
和2 作为参数
15传递给子线程,之后主线程将第一个子线程分离,将第二个子线程接合,之后主线程显示
Hello!World!。两个子线程则执行相同的线程函数,先将参数 pvoid 转换成
int 型,然后
再显示 Hello!World!,其中加入了创建线程时传递的线程编号。
读者可以尝试将 pthread_join 语句去掉,或者换成 pthread_exitNULL,看看会是怎样的
效果。
3.3 线程通信
3.3.1互斥量
互斥量是一种特殊的变量,它可以处于锁定状


发布评论