linux 下多线程编程 linux下的多线程编程
第一章 线程基础
multithreading可以被翻译成多线程控制。与传统的UNIX不同,一个传统
的UNIX进程包含一个单线程,而多线程(MT)则把一个进程分成很多可执行线
程,每一个线程都独立运行。
阅读本章可以让你理解:
Defining Multithreading Terms
Benefiting From Multithreading
Looking At Multithreading Structure
Meeting Multithreading Standards
因为多线程可以独立运行,用多线程编程可以
1) 提高应用程序响应;
2) 使多CPU系统更加有效;
3) 改善程序结构;
4) 占用更少的系统资源;
5) 改善性能;
1.1定义多线程术语:
线程:在进程的内部执行的指令序列;
单线程:单线程;
多线程:多线程;
用户级线程:在用户空间内的由线程函数库进程控制的现成;
轻进程:又称LWP,内核内部的执行核代码和系统调用的线程;
绑定(bound)线程:永远限制在LWP内的线程;
非绑定(unbound)线程:在LWP动态捆绑和卸绑的线程;
记数信号量:一个基于内存的同步机制;
1.1.1定义同时(concurrency)和并行(parallism):
在进程内至少同时有两个线程进行(process)时存在同时性问题;至少
同时有两个线程在执行时存在并行问题;
在单处理器上执行的多线程的进程内部,处理器可以在线程中间切换执行,
这样实现了同时执行;在共享内存多处理器上执行的同一个多线程进程,每一
个线程可以分别在不同的处理器上进行,是为并行。
当进程里的线程数不多于处理器的数量时,线程支持系统和操作系统保
证线程在不同的处理器上执行。例如在一个m处理器和m线程运行一个矩阵乘法,
每一个线程计算一列。
1.2多线程的益处
1.2.1提高应用程序响应
任何一个包含很多互不关联的操作(activity)的程序都可以被重新设计,
使得每一个操作成为一个线程。例如,在一个GUI(图形用户界面)内执行一
个操作的同时启动另外一个,就可以用多线程改善性能。
1.2.2使多处理器效率更高
典型情况下,有同时性需求的多线程应用程序不需要考虑处理器的数量。
应用程序的性能在被多处理器改善的同时对用户是透明的。
数学计算和有高度并发性需求的应用程序,比如矩阵乘法,在多处理器平
台上可以用多线程来提高速度。
1.2.3改善程序结构
许多应用程序可以从一个单一的、巨大的线程改造成一些独立或半独立的
执行部分,从而得到更有效的运行。多线程程序比单线程程序更能适应用户需
求的变更。
1.2.4占用较少的系统资源
应用程序可以通过使用两个或更多的进程共享内存的办法来实现多于一个
现成的控制。然而,每一个进程都要有一个完整的地址空间和操作系统状态表
项。用于创建和维护多进程大量的状态表的开销与多线程方法相比,在时间上
和空间上都更为昂贵。而且,进程所固有的独立性使得程序员花费很多精力来
实现进程间的通信和同步。
1.2.5把线程和RPC结合起来
把多线程和RPC(remote procedure call,远程过程调用)结合起来,你
可以使用没内存共享的多处理器(比方说一个工作站组)。这种结构把这组工
作站当作一个大的多处理器系统,使应用程序分布得更加容易。
例如,一个线程可以创建子线程,每一个子进程可以做RPC,调用另外一
台机器上的过程。尽管最早的线程仅仅创建一些并行的线程,这种并行可以包
括多台机器的运行。
1.2.6提高性能
本部分的性能数据是从SPARC station2(Sun 4/75)上采集的。测量精度
为微秒。
1. 线程创建时间
表1-1显示了使用thread package做缓存的缺省堆栈来创建线程的时
间。时间的测量仅仅包括实际的生成时间。不包括切换到线程的时间。比
率(ratio)列给出了该行生成时间与前一行的比。
数据表明,线程是更加经济的。创建一个新进程大概是创建一个
unbound线程的30倍,是创建一个包含线程和LWP的bound线程的5倍。
Table 1-1 Thread Creation Times
Operation Microseconds Ritio
Create unbound thread 52 -
Create bound thread 350 6.7
Fork() 1700 32.7
2. 线程同步(synchronization)时间
表1-2列出了两个线程使用pv操作的同步时间。
Table 1-2 Thread Synchronization Times
Operation Microseconds Ratio
Unbound thread 66 -
Bound thread 390 5.9
Between Processes 200 3
1.3多线程结构一览
传统的UNIX支持现成概念--每一个进程包含一个单线程,所以用多进程就
是使用多线程。但是一个进程还有一个地址空间,创建一个新进程意味着需要
创建一个新的地址空间。
因此,创建一个进程是昂贵的,而在一个已经存在的进程内部创建线程是
廉价的。创建一个线程的时间比创建一个进程的时间要少成千倍,部分是因为
在线程间切换不涉及地址空间的切换。
在进程内部的线程间通信很简单,因为线程们共享所有的东西--特别是地
址空间。所以被一个线程生成的数据可以立刻提供给其他线程。
支持多线程的接口(界面)是通过一个函数库libthread实现的。多线程通
过把内核级资源和用户级资源独立开来提供了更多的灵活性。
1.3.1用户级线程
线程仅仅在进程内部是可见的,在进程内部它们共享诸如地址空间、已经
打开的文件等所有资源。以下的状态是线程私有的,即每一个线程的下列状态
在进程内部是唯一的。
.线程号(Thread ID)
.寄存器状态(包括程序计数器和堆栈指针)
.堆栈
.信号掩码(Signal mask)
.优先级(Priority)
.线程私有的存储段(Thread-private storage)
因为线程共享进程的执行代码和大部分数据,共享数据被一个线程修改之
后可以进程内的其他线程见到。当一个进程内部线程与其他线程通信的时候,
可以不经过操作系统。
线程是多线程编程的主要主要借口。用户级的线程可以在用户空间操作,
从而避免了与内核之间的互相切换。一个应用程序可以拥有几千个线程而不占
用太多的内核资源。占用内核资源的多少主要决定于应用程序本身。
在缺省情况下,线程是非常轻便的。但是,为了控制一个线程(例如,更
多地控制进程调度策略),应用程序应当绑定线程。当一个应用程序把线程的所
有执行资源绑定后,线程就变成了内核资源(参见第9页"bound 线程")。
总之,solaris用户级线程是:
.创建的低开销,因为只在运行是占用用户地址空间的虚拟内存的几
个bit。
.快速同步,因为同步是在用户级进行,不需要接触到内核级。
.可以通过线程库libthread很容易地实现。
图1-1 多线程系统结构(略)
1.3.2轻进程(Lightweight Porcesses:LWP)
线程库采用内核支持的称为轻进程的底层控制线程。你可以把LWP看作一个
可以执行代码和系统调用的虚拟的CPU。
大多数程序员使用线程是并不意识到LWP的存在。下面的内容仅仅帮助理解
bound和unbound线程之间的区别。
------------------------------------
NOTE:Solaris2.x的LWP不同于SunOs4.0的LWP库,后者在solaris2.x中不再被支持
。
------------------------------------
类似于在stdio中fopen和fread调用open和read,线程接口调用LWP接口,
原因是一样的。
LWP建立了从用户级到内核级的桥梁。每个进程包含了一个或更多LWP,每个
LWP运行着一个或多个用户线程。创建一个现成通常只是建立一个用户环境
(context),而不是创建一个LWP。
在程序员和操作系统的精心设计下,用户级线程库保证了可用的LWP足够驱动
当前活动的用户级线程。但是,用户线程和LWP之间不是一一对应的关系,用户级
线程可以在LWP之间自由切换。
程序员告诉线程库有多少线程可以同时"运行"。例如,如果程序员指定最多有
三个线程可以同时运行,至少要有3个可用的LWP。如果有三个可用的处理器,线程
将并行进行。如果这里只有一个处理器,操作系统将在一个处理器上运行三个LWP
。
如果所有的LWP阻塞,线程库将在缓冲池内增加一个LWP。
当一个用户线程由于同步原因而阻塞,它的LWP将移交给下一个可运行的线程
。
这种移交是通过过程间的连接(coroutine linkage),而不是做系统调用而完成
。
操作系统决定哪一个LWP什么时候在哪一个处理器上运行。它不考虑进程中线
程的类型和数量。内核按照LWP的类型和优先级来分配CPU资源。线程库按照相同
的方法来为线程分配LWP。每个LWP被内核独立地分发,执行独立的系统调用,引
起独立的页错误,而且在多处理器的情况下将并行执行。
一些特殊类型的LWP可以不被直接交给线程。(!?不明)
1.3.3非绑定线程Unbound Threads
在LWP缓冲池中排队的线程称为unbound thread。通常情况下我们的线程都是
unbound的,这样他们可以在LWP之间自由切换。
线程库在需要的时候激活LWP并把它们交给可以执行的线程。LWP管理线程的
状态,执行线程的指令。如果线程在同步机制中被阻塞,或者其他线程需要运行,
线程状态被存在进程内存中,LWP被移交给其他线程。
1.3.4绑定线程Bound Threads
如果需要,你可以将一个线程绑定在某个LWP上。
例如,你可以通过绑定一个线程来实现:
1. 将线程全局调度(例如实时)
2. 使线程拥有可变的信号栈
3. 给线程分配独立的定时器和信号(alarm)
在线程数多于LWP时,bounded比unbound线程体现出一些优越性。
例如,一个并行的矩阵计算程序在每个线程当中计算每一行。如果每个处理器
都有一个LWP,但每个LWP都要处理多线程,每个处理器将要花费相当的时间来切换
线程。在这种情况下,最好使每个LWP处理一个线程,减少线程数,从而减少线程
切换。
在一些应用程序中,混合使用bound和unbound线程更加合适。
例如,有一个实时的应用程序,希望某些线程拥有全局性的优先级,并被实时
调度,其他线程则转入后台计算。另一个例子是窗口系统,大多数操作都是
unbound的,但鼠标操作需要占用一个高优先级的,bound的,实时的线程。
1.4多线程的标准
多线程编程的历史可以回溯到二十世纪60年代。在UNIX操作系统中的发展是从
80年代中期开始的。也许是令人吃惊的,关于支持多线程有很好的协议,但是今
天我们仍然可以看到不同的多线程开发包,他们拥有不同的接口。
但是,某几年里一个叫做POSIX1003.4a的小组研究多线程编程标准。当标准完
成后,大多数支持多线程的系统都支持POSIX接口。很好的改善了多线程编程的可
移植性。
solaris多线程支持和POSIX1003.4a没有什么根本性的区别。虽然接口是不同
的,但每个系统都可以容易地实现另外一个系统可以实现的任何功能。它们之间没
有兼容性问题,至少solaris支持两种接口。即使是在同一个应用程序里,你也可
以混合使用它们。
用solaris线程的另一个原因是使用支持它的工具包,例如多线程调试工具
(multighreaded debugger)和truss(可以跟踪一个程序的系统调用和信号),
可以很好地报告线程的状态。
第二章:用多线程编程
2.1线程(函数)库(The Threads Library)
用户级多线程是通过线程库,libthread来实现的(参考手册第3页:
library routines)。线程库支持信号,为可运行的程序排队,并负责同
时操纵多任务。
这一章讨论libthread中的一些通用过程,首先接触基本操作,然后循
序渐进地进入更复杂的内容
创建线程-基本特性 Thr_create(3T)
获得线程号 Thr_self(3T)
执行线程 Thr_yield(3T,the below is same)
挂起或继续线程 Thr_suspend
Thr_continue
向线程送信号 Thr_kill
设置线程的调用掩模 Thr_sigsetmask
终止线程 Thr-exit
等待线程终止 Thr-join
维护线程的私有数据 Thr_keycreate
Thr_setspecific
Thr_getspecific
创建线程-高级特性 Thr_create
获得最小堆栈容量 Thr_min_stack
获得或设置线程的同时性等级 Thr_getconcurrency
Thr_setconcurrency
获得或设置线程的优先级 Thr_getprio
Thr_setprio
2.1.1创建线程-基本篇
thr_create过程是线程库所有过程当中最复杂的一个。这部分的内容仅
适用于你使用thr_create的缺省参数来创建进程。
对于thr_create更加复杂的使用,包括如何使用自定参数,我们将在高
级特性部分给出说明。
thr_create(3T)
这个函数用于在当前进程中添加一个线程。注意,新的线程不继承未处
理的信号,但继承优先级和信号掩模。
#include
int thr_create(void *stack_base,size_t stack_size,
void *(*start_routine) (void*),void *arg,long flags,
thread_t *new_thread);
size_t thr_min_stack(void);
stack_base--新线程的堆栈地址。如果stack_base是空则thr_create()按
照stack_size为新线程分配一个堆栈。
Stack_size--新线程堆栈的字节数。如果本项为0,将使用缺省值,一般
情况下最好将此项设为0。
并不是每个线程都需要指定堆栈空间。线程库为每个线程的堆栈分配1M
的虚拟内存,不保留交换空间。(线程库用mmap(2)的MAP_NORESERVE的选项
来实现这种分配)。
Start_routine--指定线程开始执行的函数。如果start_routine返回,
线程将用该函数的返回值作为退出状态而退出。(参考thr_exit(3T))。
Flags--指定新线程的属性,一般设置为0。
Flags的值是通过下列内容的位同或来实现的(最后四个flags在高级特性中
给出)。
1. THR_DETACHED 将新线程分离,使得它的线程号和其他资源在线程
结束时即可以回收利用。当你不想等待线程终止时,将其置位。如果没有明确的
同步需求阻碍,一个不挂起的,分离的线程可以在创建者的thr_create返回之前
终止并将其线程号分配给一个心得线程。
2. THR_SUSPENDED挂起新线程,直到被thr_continue唤醒。
3. THR_BOUND把新线程永久绑定在一个LWP上(生成一个绑定线程)。
4. THR_NEW_LWP将非绑定线程的同时性级别加1。
5. THR_DAEMON新线程为一个守护线程。
New_thread--指向存储新线程ID的地址。多数情况下设置为0。
Return Values--thr_create()在成功执行后返回0并退出。任何其他返回值
表明有错误发生。当以下情况被检测到时,thr_create()失败并返回响应的值。
EAGAIN :超出了系统限制,例如创建了太多的LWP。
ENOMEM:可用内存不够创建新线程。
EINVAL:stack_base不是NULL而且stack_size比thr_minstack()函数返回的
最小堆栈要小。
2.1.2获取线程号
thr_self(3T) 获得自身的线程号。
#include
thread_t thr_self(void)
返回值--调用者的线程号。
2.1.3放弃执行
thr_yield(3T)
thr_yield停止执行当前线程,将执行权限让给有相同或更高优先权的线程。
#include
void thr_yield(void);
2.1.4挂起或继续执行线程
thr_suspend(3T) 挂起线程。
#include
int thr_suspend(thread_t target_thread);
thr_suspend()立即挂起由target_thread指定的线程。在thr_suspend成功
返回后,挂起的线程不再执行。后继的thr_suspend无效。
Return Values--执行成功后返回0。其他返回值意味着错误。以下情况发生
时,thr_suspend()失败并返回相关值。
ESRCH: 在当前进程中找不到target_thread。
Thr_continue(3T)
Thr_continue()恢复执行一个挂起的线程。一旦线程脱离挂起状态,后继的
thr_continue将无效。
#include
int thr_continue(thread_t target_thread);
一个挂起的线程不会被信号唤醒。信号被挂起知道线程被thr-continue恢复
执行。
返回值--成功执行后返回0。其他值意味着错误。在以下情况发生时,函数失
败并返回相关值。
ESRCH:target_thread在当前进程中找不到。
2.1.5向线程发信号
thr_kill(3T)向线程发信号
#include
#include
int thr_kill(thread_t target_thread,int sig);
thr_kill向线程号为target_thread的线程发送信号sig。Target_thread一定
要与调用线程处于同一个进程内。参数sig一定是signal(5)中定义过的。
当sig是0时,错误检查将被执行,没有实际的信号被发送。这可以用来检测
target_thread参数是否合法。
返回值--成功执行后返回0,其他值意味着错误。在以下情况发生时,函数失
败并返回相关值。
EINVAL:sig非法;
ESRCH:target_thread找不到;
2.1.6设置本线程的信号掩模
thr_sigsetmask(3T) 获取或改变本线程的信号掩模(signal mask)
#include
#include
int thr_sigsetmask(int how,const sigset_t *set,sigset_t *oset);
how参数决定信号设置将被如何改变,可以是下列值之一:
SIG_BLOCK--在当前信号掩模上增加set,set指要阻塞的信号组。
SIG_UNBLOCK--在当前信号掩模上去掉set,set指要解除阻塞的信号组。
SIG_SETMASK--用新的掩模代替现有掩模,set指新的信号掩模。
当set的值是NULL时,how的值并不重要,信号掩模将不被改变。所以,要查
询当前的信号掩模,就给set赋值为NULL。
当参数oset不是NULL时,它指向以前的信号掩模存放的地方。
Return Values--正常执行后返回0。其他值意味着错误。在以下情况发生时,
函数失败并返回相关值。
EINVAL:set不是NULL且how没有被定义;
EFAULT:set或oset不是合法地址;
2.1.7终止线程
thr_exit(3T)
用来终止一个线程。
#include
void thr_exit(void *status);
thr_exit 函数终止当前线程。所有的私有数据被释放。如果调用线程不是一
个分离线程,线程的ID和返回状态保留直到有另外的线程在等待。否则返回状态
被忽略,线程号被立刻重新使用。
返回值--当调用线程是进程中的最后一个非守护线程,进程将用状态0退出。
当最初的线程从main()函数中返回时进程用该线程main函数的返回值退出。
线程可以通过两种方式停止执行。第一种是从最初的过程中返回。第二种是
提供一个退出代码,通过调用thr_exit()结束。下面的事情依赖于在线程创建时
flags的设置。
线程A终止的缺省操作(当flags的相应位设为0时,执行缺省操作)是保持
状态,直到其它线程(不妨设为B)通过"联合"的方式得知线程A已经死亡。联合
的结果是B线程得到线程A的退出码,A自动消亡。你可以通过位或来给flags的
THR_DETACHED参数置位,使得线程在thr_exit()之后或从最初过程返回后立即消
亡。在这种情况下,它的退出码不会被任何线程获得。
有一个重要的特殊情况,在主线程--即最初存在的线程--从主函数返回或调
用了exit(),整个进程将终止。所以在主线程中要注意不要过早地从主函数main
返回。
如果主线程仅仅调用了thr_exit(),仅仅是它自己死亡,进程不会结束,进
程内的其他线程将继续运行(当然,如果所有的线程都结束,进程也就结束了)。
如果一个线程是非分离的,在它结束后一定要有其它进程与它"联合",否则
该线程的资源就不会被回收而被新线程使用。所以如果你不希望一个线程被
"联合",最好按照分离线程来创建。
另外一个flag参数是THR_DAEMON。使用这个标志创建的线程是守护线程,在
其他线程终止之后,这些线程自动终止。这些守护线程在线程库内部特别有用。
守护线程可以用库内函数创建--在程序的其他部分是不可见的。当程序中所
有的其他线程终止,这些线程自动终止。如果它们不是守护线程,在其它线程终
止后他们不会自动终止,进程不会自动结束。
2.1.8等待线程结束
thr_join(3T) 用thr_join函数来等待线程终止。
#include
int thr_join(thread_t wait_for,thread_t *departed,void **status);
thr_join()函数阻塞自身所在的线程,直到由wait_for指定的线程终止。指
定的线程一定与本线程在同一个进程内部,而且一定不是分离线程。当wait_for
参数为0时,thr_join等待任何一个非分离线程结束。换句话说,当不指定线程
号时,任何非分离线程的退出将导致thr_join()返回。
当departed参数不是NULL时,在thr_join正常返回时它指向存放终止线程ID
的地址。当status参数不是NULL时,在thr_join正常返回时它指向存放终止线程
退出码的地址。
如果线程创建时指定了堆栈,在thr_join返回时堆栈可以被回收。由它返回
的线程号可以被重新分配。
不能有两个线程同时等待同一个线程,如果出现这种情况,其中一个线程正
常返回,另外一个返回ESRCH错误。
返回值--thr_join()在正常执行后返回0,其他值意味着错误。在以下情况
发生时,函数失败并返回相关值。
ESRCH wait_for不合法,等待的线程为分离现成。
EDEADLK 等待自身结束。
最后步骤
thr_join()有三个参数,提供了一定的灵活性。当你需要一个线程等待
直到另外一个指定的线程结束,应当把后者的ID提供为第一参数。如果
需要等待到任何其他的线程结束,将第一参数置零。
如果调用者想知道是那个线程终止,第二参数应当是储存死线程的ID的地址。
如果不感兴趣,将该参数置零。最后如果需要知道死线程的退出码,应当指出接
收该错误码的地址。
一个线程可以通过以下的代码等待所有的非守护线程结束:
while(thr_join(0,0,0)==0)
第三个参数的声明(void **)看上去很奇怪。相应的thr_exit()的参数为
void *。这样做的意图在于你的错误代码为定长的四字节,c语言给定长4字节的
定义不能是void型,因为这以为着没有参数。所以用void*。因为thr_join()的
第三参数必须是一个指向thr_exit()返回值的指针,所以类型必须是void **。
注意,thr_join()只在目标线程为非分离时有效。如果没有特殊的同步要求
的话,线程一般都设置成分离的。
可以认为,分离线程是通常意义下的线程,而非分离线程知识特殊情况。
2.1.9简单的例程
在例子2-1里,一个运行在顶部的线程,创建一个辅助线程来执行fetch过程,
这个辅助过程涉及到复杂的数据库查询,需要较长的时间。主线程在等待结果的
时候还有其他事情可做。所以它通过执行thr_join()来等待辅助过程结束。
操作结果被当作堆栈参数传送,因为主线程等待spun-off线程结束。在一般
意义上,用malloc()存储数据比通过线程的堆栈来存储要好一些。????
Code Example 2-1 A Simple Threads Program
Void mainline(){
Char int result;
Thread_t helper;
Int status;
Thr_create(0,0,fetch,&result,0,&helper);
/* do something else for a while */
Thr_join(helper,0,&status);
/* it's now safe to use result*/
}
void fetch(int * result){
/*fetch value from a database */
*result=value;
thr_exit(0);
}
2.1.10维护线程专有数据
单线程C程序有两种基本数据--本地数据和全局数据。多线程C程序增加了
一个特殊类型--线程专有数据(TSD)。非常类似与全局数据,只不过它是线程
私有的。
TSD是以线程为界限的。TSD是定义线程私有数据的唯一方法。每个线程专有
数据项都由一个进程内唯一的关键字(KEY)来标识。用这个关键字,线程可以
来存取线程私有的数据。
维护TSD的方法通过以下三个函数进行:
· thr_keycreate()--创建关键字
· thr_setspecific()--将一个线程绑定在一个关键字上
· thr_getspecific()--存储指定地址的值
2.1.10.1 thr_keycreate(3T)
thr_keycreate()在进程内部分配一个标识TSD的关键字。关键字是进程内
部唯一的,所有线程在创建时的关键字值是NULL。
一旦关键字被建立,每一个线程可以为关键字绑定一个值。这个值对于绑
定的线程来说是唯一的,被每个线程独立维护。
#include
int thr_keycreate(thread_key_t keyp,
void (*destructor)(void *value);
如果thr_keycreate()成功返回,分配的关键字被存储在由keyp指向的区
域里。调用者一定要保证存储和对关键字的访问被正确地同步。
一个可选的析构函数,destructor,可以和每个关键字联系起来。如果一
个关键字的destructor不空而且线程给该关键字一个非空值,在线程退出时该
析构函数被调用,使用当前的绑定值。对于所有关键字的析构函数执行的顺序
是不能指定的。
返回值--thr_keycreate()在正常执行后返回0,其他值意味着错误。在以
下情况发生时,函数失败并返回相关值。
EAGAIN 关键字的名字空间用尽
ENOMEM 内存不够
2.1.10.2 Thr_setspecific(3T)
#include
int thr_setspecific(thread_key_t key,void *value);
thr_setspecific()为由key指定的TSD关键字绑定一个与本线程相关的值。
返回值--thr_setspecific在正常执行后返回0,其他值意味着错误。在以
下情况发生时,函数失败并返回相关值。
ENOMEM 内存不够
EINVAL 关键字非法
2.1.10.3 Thr_getspecific(3T)
#include
int thr_getspecific(thread_key_t key,void **valuep);
thr_getspecific()将与调用线程相关的关键字的值存入由valuep指定的区
域。
返回值--thr_getspecific()在正常执行后返回0,其他值意味着错误。在
以下情况发生时,函数失败并返回相关值。
EINVAL 关键字非法。
2.1.10.5 全局和私有的线程专有数据
例程2-2是从一个多线程程序中摘录出来的。这段代码可以被任意数量的线
程执行,但一定要参考两个全局变量:errno和mywindow,这两个值是因线程而
异的,就是说是线程私有的。
Code Example 2-2 线程专有数据--全局且私有的
Body(){
……
while(srite(fd,buffer,size)==-1){
if(errno!=EINTR){
fprintf(mywindow,"%sn",
strerror(errno));
exit(1);
}
}
………
}
本线程的系统错误代码errno可以通过线程的系统调用来获得,而不是通过
其他线程。所以一个线程获得的错误码与其他线程是不同的。
变量mywindow指向一个线程私有的输入输出流。所以,一个线程的mywindow
和另外一个线程是不同的,因而最终体现在不同的窗口里。唯一的区别在于线程
库来处理errno,而程序员需要精心设计mywindow。
下面一个例子说明了mywindow的设计方法。处理器把mywindow的指针转换
成为对_mywindow过程的调用。
然后调用thr_getspecific(),把全程变量mywindow_key和标识线程窗口的
输出参数win传递给它。
Code Example 2-3 将全局参考转化为私有参考
#define mywindow _mywindow()
thread_key_t mywindow_key;
FILE * _mywindow(void){
FILE *win;
Thr_getspecific(mywindow_key,&win);
Return(win);
}
void thread_start(…){
…
make_mywindow();
…
}
变量mywindow标识了一类每个线程都有私有副本的变量;就是说,这些变量
是线程专有数据。每个线程调用make_mywindow()来初始化自己的窗口,并且生
成一个指向它的实例mywindow。
一旦过程被调用,现成可以安全地访问mywindow,在_mywindow函数之后,线
程可以访问它的私有窗口。所以,对mywindow的操作就象是直接操作线程私有
数据一样。
Code Example 2-4 显示了怎样设置
Code Example 2-4 初始化TSD
Void make_mywindow(void){
FILE **win;
Static int once=0;
Static mutex_t lock;
Mutex_lock(&lock);
If (!once){
Once=1;
Thr_keycreate(&mywindow_key,free_key);
}
mutext_unlock(&lock);
win=malloc(sizeof(*win));
create_window(win,…);
thr_setspecific(mywindow_key,win);
}
void freekey(void *win){
free(win);
}
首先,给关键字mywindow_key赋一个唯一的值。这个关键字被用于标识
TSD。所以,第一个调用make_mywindow的线程调用thr_keycreate(),这个函
数给其第一个参数赋一个唯一的值。第二个参数是一个析构函数,用来在线程
终止后将TSD所占的空间回收。
下一步操作是给调用者分配一个TSD的实例空间。分配空间以后,调用
create_window过程,为线程建立一个窗口并用win来标识它。最后调用
thr_setspecific(),把win(即指向窗口的存储区)的值与关键字绑在一起。
做完这一步,任何时候线程调用thr_getspecific(),传送全局关键字,
它得到的都是该线程在调用thr_setspecific时与关键字绑定的值。
如果线程结束,在thr_keycreate()中建立的析构函数将被调用,每个析构
函数只有在终止的线程用thr_setspecific()为关键字赋值之后才会执行。
2.1.11创建线程--高级特性
2.1.11.1 thr_create(3T)
#include
int thr_create(void *stack_base,size_t stack_size,
void *(*start_routine)(void *),void * arg,
long flags,thread_t *newthread);
size_t thr_min_stack(void);
stack_base--新线程所用的堆栈地址。如果本参数为空,thr_create
为新线程分配一个至少长stack_size的堆栈。
Stack_size--新线程使用堆栈的字节数。如果本参数为零,将使用缺省值。
如果非零,一定要比调用thr_min_stack()获得的值大。
一个最小堆栈也许不能容纳start_routine需要的堆栈大小,所以如果
stack_size被指定,一定要保证它是最小需求与start_routine及它所调用的
函数需要的堆栈空间之和。
典型情况下,由thr_create()分配的线程堆栈从一个页边界开始,到离指
定大小最接近的页边界结束。在堆栈的顶部放置一个没有访问权限的页,这样,
大多数堆栈溢出错误发生在向越界的线程发送SIGSEGV信号的时候。由调用者分
配的线程堆栈 are used as is . ????
如果调用者使用一个预分配的堆栈,在指向该线程的thr_join()函数返回
之前,堆栈将不被释放,即使线程已经终止。然后线程用该函数的返回值作为
退出码退出。
通常情况下,你不需要为线程分配堆栈空间。线程库为每个线程的堆栈分
配一兆的虚拟内存,不保留交换空间(线程库用mmap(2)的MAP_NORESERVE选项
来进行分配)。
每个用线程库创建的线程堆栈有一个"红区"。线程库将一个红区放置在堆
栈顶部来检测溢出。该页是没有访问权限的,在访问时将导致一个页错误。红
区被自动附加在堆栈顶端,不管是用指定的容量还是缺省的容量。
只有在你绝对确信你给的参数正确之后才可以指定堆栈。没有多少情况需
要去指定堆栈或它的大小。即使是专家也很难知道指定的堆栈和容量是否正确。
这是因为遵循ABI的程序不能静态地决定堆栈的大小。它的大小依赖于运行时的
环境。
2.1.11.2建立你自己的堆栈
如果你指定了线程堆栈的大小,要保证你考虑到了调用它的函数和它调用
的函数需要的空间。需要把调用结果、本地变量和消息结构的成分都考虑进来。
偶尔你需要一个与缺省堆栈略有不同的堆栈。一个典型的情况是当线程需
要一兆以上的堆栈空间。一个不太典型的情况是缺省堆栈对于你来说太大了。
你可能会创建上千个线程,如果使用缺省堆栈时,就需要上G的空间。
堆栈的上限是很显然的,但下限呢?一定要有足够的堆栈空间来保存堆栈
框架和本地变量。
你可以用thr_min_stack()函数来获得绝对的最小堆栈容量,它返回运行一
个空过程所需要的堆栈空间。有实际用途的线程需要的更多,所以在减小线程
堆栈的时候要小心。
你通过两种方式指定一个堆栈。第一种是给堆栈地址赋空值,由实时的运
行库来为堆栈分配空间,但需要给stack_size参数提供一个期望的值。
另外一种方式是全面了解堆栈管理,为thr_create函数提供一个堆栈的指
针。这意味着你不但要负责为堆栈分配空间,你还要考虑在线程结束后释放这
些空间。
在你为自己的堆栈分配空间之后,一定要调用一个mprotect(2)函数来为它
附加一个红区。
Start_routine--指定新线程首先要执行的过程。当start_routine返回时,
线程用该返回值作为退出码退出(参考thr_exit(3T))。
注意,你只能指定一个参数。如果你想要多参数,把他们作成一个(例如
写入一个结构)。这个参数可以是任何一个由void说明的数据,典型的是一个
4字节的值。任何更大的值都需要用指针来间接传送。
Flags--指定创建线程的属性。在多数情况下提供0即可。
Flags的值通过位或操作来赋。
THR_SUSPENDED--新线程挂起,在thr_continue()后再执行
start_routine。用这种办法在运行线程之前对它进行操作(例如改变
优先级)。分离线程的终止被忽略。
THR_DETACHED--将新线程分离,使线程一旦终止,其资源可以得到立刻
回收利用。如果你不需要等待线程结束,设置此标志。
如果没有明确的同步要求,一个不挂起的,分离的线程可以在它
的创建者调用的thr_create函数返回之前终止并将线程号和其他资源
移交给其他线程使用。
THR_BOUND--将一个新线程永久绑定在一个LWP上(新线程为绑定线程)。
THR_NEW_LWP--给非绑定线程的同时性等级加1。效果类似于用
thr_setconcurrency(3T)来增加同时性等级,但是使用
thr_setconcurrency()不影响等级设置。典型的,THR_NEW_LWP在LWP池
内增加一个LWP来运行非绑定线程。
如果你同时指定了THR_BOUND和THR_NEW_LWP,两个LWP被创建,一
个被绑定在该线程上,另外一个来运行非绑定线程。
THR_DAEMON--标志新线程为守护线程。当所有的非守护线程退出后进程
结束。守护线程不影响进程退出状态,在统计退出的线程数时被忽略。
一个进程可以通过调用exit(2)或者在所有非守护线程调用
thr_exit(3T)函数终止的时候终止。一个应用程序,或它调用的一个库,
可以创建一个或多个在决定是否退出的时候被忽略的线程。用
THR_DAEMON标志创建的线程在进程退出的范畴不被考虑。
New_thread--在thr_create()成功返回后,保存指向存放新线程ID的地址。
调用者负责提供保存这个参数值指向的空间。
如果你对这个值不感兴趣,给它赋值0。
返回值--thr_thread在正常执行后返回0,其他值意味着错误。在以下情况
发生时,函数失败并返回相关值。
EAGAIN 超过系统限制,例如创建了太多的LWP。
ENOMEM 内存不够创建新线程。
EINVAL stack_base非空,但stack_size比thr_minstack()的返回值小。
2.1.11.3 Thr_create(3T)例程
例2-5显示了怎样用一个与创建者(orig_mask)不同的新的信号掩模来创建
新线程。
在这个例子当中,new_mask被设置为屏蔽SIGINT以外的任何信号。然后创建
者的信号掩模被改变,以便新线程继承一个不同的掩模,在thr_create()返回后,
创建者的掩模被恢复为原来的样子。
例子假设SIGINT不被创建者屏蔽。如果最初是屏蔽的,用相应的操作去掉屏
蔽。另外一种办法是用新线程的start routine来设置它自己的信号掩模。
Code Example 2-5 thr_create() Creates Thread With New Signal Mask
thread_t tid;
sigset_t new_mask, orig_mask;
int error;
(void)sigfillset(&new_mask);
(void)sigdelset(&new_mask, SIGINT);
(void)thr_sigsetmask(SIGSETMASK, &new_mask, &orig_mask):
error = thr_create(NULL, 0, dofunc, NULL, 0, &tid);
(void)thr_sigsetmask(SIGSETMASK, NULL, &orig_mask);
2.1.12获得最小堆栈
thr_min_stack(3T) 用thr_min_stack(3T)来获得线程的堆栈下限
#include
size_t thr_min_stack(void);
thr_min_stack()返回执行一个空线程所需要的堆栈大小(空线程是一个创
建出来执行一个空过程的线程)。
如果一个线程执行的不仅仅是空过程,应当给它分配比thr_min_stack()返
回值更多的空间。
如果线程创建时由用户指定了堆栈,用户应当为该线程保留足够的空间。在
一个动态连接的环境里,确切知道线程所需要的最小堆栈是非常困难的。
大多数情况下,用户不应当自己指定堆栈。用户指定的堆栈仅仅用来支持那
些希望控制它们的执行环境的应用程序。
一般的,用户应当让线程库来处理堆栈的分配。线程库提供的缺省堆栈足够
运行任何线程。
2.1.13设置线程的同时性等级
2.1.13.1 thr_getconcurrency(3T)
用thr_getconcurrency()来获得期望的同时性等级的当前值。实际上同时活
动的线程数可能会比这个数多或少。
#include
int thr_getconcurrency(void)
返回值--thr_getconcurrency()为期望的同时性等级返回当前值。
2.1.13.2 Thr_setconcurrency(3T)
用thr_setconcurrency()设置期望的同时性等级。
#include
int thr_setconcurrency(new_level)
进程中的非绑定线程可能需要同时活动。为了保留系统资源,线程系统的缺
省状态保证有足够的活动线程来运行一个进程,防止进程因为缺少同时性而死锁。
因为这也许不会创建最有效的同时性等级,thr_setconcurrency()允许应用
程序用new_level给系统一些提示,来得到需要的同时性等级。
实际的同时活动的线程数可能比new_level多或少。
注意,如果没有用thr_setconcurrency调整执行资源,有多个
compute-bound(????)线程的应用程序将不能分配所有的可运行线程。
你也可以通过在调用thr_create()时设置THR_NEW_LWP标志来获得期望的同
时性等级。
返回值--thr_setconcurrency()在正常执行后返回0,其他值意味着错误。
在以下情况发生时,函数失败并返回相关值。
EAGAIN 指定的同时性等级超出了系统资源的上限。
EINVAL new_level的值为负。
2.1.14得到或设定线程的优先级
一个非绑定线程在调度时,系统仅仅考虑进程内的其他线程的简单的优先级,
不做调整,也不涉及内核。线程的系统优先级的形式是唯一的,在创建进程时继
承而来。
2.1.14.1 Thr_getprio(3T)
用thr_getprio()来得到线程当前的优先级。
#include
int thr_getprio(thread_t target_thread,int *pri)
每个线程从它的创建者那里继承优先级,thr_getprio把target_thread当前
的优先级保存到由pri指向的地址内。
返回值--thr_getprio()在正常执行后返回0,其他值意味着错误。在以下情
况发生时,函数失败并返回相关值。
ESRCH target_thread在当前进程中不存在。
2.1.14.2 Thr_setprio(3T)
用thr_setprio()来改变线程的优先级。
#include
int thr_setprio(thread_t target_thread,int pri)
thr_setprio改变用target_thread指定的线程的优先级为pri。缺省状态下,
线程的调度是按照固定的优先级--从0到最大的整数--来进行的,即使不全由优先
级决定,它也占有非常重要的地位。Target_thread将打断低优先级的线程,而让
位给高优先级的线程。
返回值--thr_setprio()在正常执行后返回0,其他值意味着错误。在以下情
况发生时,函数失败并返回相关值。
ESRCH target_thread在当前进程中找不到。
EINVAL pri的值对于和target_thread相关的调度等级来说没有意义。
2.1.15线程调度和线程库函数
下面的libthread函数影响线程调度
2.1.15.1 thr_setprio()和thr_getprio()
这两个函数用来改变和检索target_thread的优先级,这个优先级在用户级线
程库调度线程时被引用,但与操作系统调度LWP的优先级无关。
这个优先级影响线程和LWP的结合--如果可运行的线程比LWP多的时候,高优
先级的线程得到LWP。线程的调度是"专横"的,就是说,如果有一个高优先级的线
程得不到空闲的LWP,而一个低优先级的线程占有一个LWP,则低优先级的线程被
迫将LWP让给高优先级的线程。
2.1.15.2 thr_suspend()和thr_continue()
这两个函数控制线程是否被允许运行。调用thr_suspend(),可以把线程设置
为挂起状态。就是说,该线程被搁置,即使有可用的LWP。在其他线程以该线程为
参数调用thr_continue后,线程退出挂起状态。这两个函数应当小心使用--它们
的结果也许是危险的。例如,被挂起的线程也许是处在互锁状态的,将它挂起可
能会导致死锁。
一个线程可以在创建时用THR_SUSPENDED标志设置为挂起。
2.1.15.3 thr_yield()
Thr_yield函数使线程在相同优先级的线程退出挂起状态后交出LWP。(不会有
更高优先级的线程可运行而没有运行,因为它会通过强制的方式取得LWP)。这个
函数具有非常重要的意义,因为在LWP上没有分时的概念(尽管操作系统在执行
LWP
时有分时)。
最后,应当注意priocntl(2)也会影响线程调度。更详细的内容请参照"LWP和
调度等级"。
第四章 4. 操作系统编程
本章讨论多线程编程如何和操作系统交互,操作系统作出什么改变来支持多线
程。
进程--为多线程而做的改动
警告(alarm), 计数器(interval timer), 配置(profiling)
全局跳转--setjmp(3C) 和longjmp(3C)
资源限制
LWP和调度类型
扩展传统信号
I/O 问题
4.1进程--为多线程而做的改变
4.1.1复制父线程
fork(2)
用fork(2)和fork1(2)函数,你可以选择复制所有的父线程到子线程,或者子
线程只有一个父线程???。
Fork()函数在子进程中复制地址空间和所有的线程(和LWP)。这很有用,例如,
如果子进程永远不调用exec(2)但是用父进程地址空间的拷贝。
为了说明,考虑一个父进程中的线程--不是调用fork()的那个--给一个互斥锁
加了锁。这个互斥锁被拷贝到子进程当中,但给互斥锁解锁的线程没有被拷贝。所
以子进程中的任何试图给互斥锁加锁的线程永久等待。为了避免这种情况,用fork()
复制进程中所有的线程。
注意,如果一个线程调用fork(),阻塞在一个可中断的系统调用的线程将返回
EINTR。
Fork1(2)
Fork1(2) 函数在子线程中复制完全的地址空间,但是仅仅复制调用fork1()的
线程。这在子进程在fork()之后立即调用exec()时有用。在这种情况下,子进程不
需要复制调用fork(2)函数的那个线程以外的线程。
在调用fork1()和exec()之间不要调用任何库函数--库函数也许会使用一个由
多个线程操作的锁。
*Fork(2)和fork1(2)的注意事项
对于fork()和fork1(),在调用之后使用全局声明时要小心。
例如,如果一个线程顺序地读一个文件而另外一个线程成功地调用了fork(),
每一个进程都有了一个读文件的线程。因为文件指针被两个线程共享,父进程得到
一些数据,而子进程得到另外一些。
对于fork()和fork1(),不要创建由父进程和子进程共同使用的锁。这仅发生在
给锁分配的内存是共享的情况下(用mmap(2)的MAP_SHARED声明过)。
Vfork(2)
Vfork(2)类似于fork1(),只有调用线程被拷贝到子进程当中去。
注意,子进程中的线程在调用exec(2)之前不要改变内存。要记住vfork()将父
进程的地址空间交给子进程。父进程在子进程调用exec()或退出后重新获得地址空
间。子进程不改变父进程的状态是非常重要的。
例如在vfork()和exec()之间创建一个新线程是危险的。
4.1.2执行文件和终止进程
exec(2)和exit(2)
exec(2)和exit(2)调用和单线程的进程没有什么区别,只是它们破坏所有线程
的地址空间。两个调用在执行资源(以及活动线程)被破坏前阻塞。
如果exec()重建一个进程,它创建一个LWP。进程从这个初始线程开始执行程序。
象平时一样,如果初始线程返回,它调用exit()来破坏整个进程。
如果所有线程退出,进程用0值退出。
4.2 Alarms(闹钟???), Interval Timers(定时器), and Profiling(配置)
每个LWP有一个唯一的实时的定时器和一个绑定在LWP上的线程的闹钟。定时器
和闹钟在到时间时向线程发送信号。
每个LWP有一个虚拟时间或一个配置定时器,绑定在该LWP上的线程可以使用它
们。如果虚拟定时器到时间,它向拥有定时器的LWP发送信号SIGVTALRM或SIGPROF,
发送哪一个视情况而定。
你可以用profil(2)给每一个LWP进行预配置,给每个LWP私有的缓冲区或者一个
LWP共享的缓冲区。配置数据按LWP用户时间的每一个时钟单位更新。在创建LWP时配
置状态被继承。
4.3非本地跳转--setjmp(3C)和longjmp(3C)
setjmp()和longjmp()的使用范围限制在一个线程里,在大多数情况下是合适的。
然而,只有setjmp()和longjmp()在同一个线程里,线程才能对一个信号执行longjmp()。
4.4资源限制
资源限制在整个进程内,每个线程都可以给进程增加资源。如果一个线程超过
了软资源限制,它将发出相应的信号。进程内可用的资源总量可以由getrusage(3B)
获得。
4.5 LWP和调度类型
Solaris 内核有3种进程调度类型。最高优先级的是实时(realtime RT)。其次
是系统(system)。系统调度类型不能在用户进程中使用。最低优先级的是分时
(timeshare TS),它也是缺省类型。
调度类型在LWP内维护。如果一个进程被创建,初始LWP继承父进程的调度类型和
优先级。如果有跟多的LWP被创建来运行非绑定线程,它们也继承这些调度类型和优先
级。进程中的所有非绑定线程有相同的调度类型和优先级。
每个调度类型按照调度类型的配置优先级,把LWP的优先级映射到一个全体的分配
优先级。???
绑定线程拥有和它们绑定的LWP相同的调度类型和优先级。进程中的每个绑定线程
有一个内核可以看到的调度类型和优先级。系统按照LWP来调度绑定线程。
调度类型用priocntl(2)来设置。前两个参数的指定决定了是只有调用的LWP还是
一个或多个进程所有的LWP都被影响。第三个参数是一个指令,它可以是以下值之一。
· PC_GETCID--获得指定类型的类型号和类型属性
· PC_GETCLINFO--获得指定类型的名称和类型属性
· PC_GETPARMS--获得类型标识和进程中,LWP,或者一组进程的因类型而异
的调度参数
· PC_SETPARMS--设置类型标识和进程中,LWP,或者一组进程的因类型而异
的调度参数
用priocntl()仅限于绑定线程。为了设置非绑定线程的优先级,使用thr_setprio(
3T)。
4.5.1分时调度
分时调度将执行资源公平地分配给各进程。内核的其他部分可以在短时间内独占
处理器,而不会使用户感到响应时间延长。
Priocntl(2)调用设置一个或多个线程的nice(2)级别。Priocntl()影响进程中所有
的分时类型的LWP的nice级别。普通拥护的nice()级别从0到20,而超级用户的进程从
-20到20。值越小,级别越高。
分时LWP的分配优先级根据它的LWP的CPU使用率和它的nice()级别来确定。Nice()
级别指定了在进程内供分时调度器参考的相对优先级。LWP的nice()值越大,所得的执
行资源越少,但不会为0。一个执行的多的LWP会被赋予比执行的少的LWP更小的优
先级。
4.5.2实时调度
实时类型可以被整个进程或进程内部的一个或多个线程来使用。这需要超级用户
权限。与分时类型的nice(2)级别不同,标识为实时的LWP可以被独立或联合地分配优先
级。一个priocntl(2)调用影响进程中所有实时的LWP的属性。
调度器总是分配最高优先级的实时LWP。如果一个高优先级的LWP可运行,它将打断
低优先级的LWP。一个有先行权(preempt)的LWP被放置在该等级队列的头上。一个实
时(RT)的LWP保持控制处理器,直到被其他线程中断时挂起,或者实时优先级改变。
RT类型的LWP对TS类型的进程有绝对的优先权。
一个新的LWP继承父线程或LWP的调度类型。一个RT类型的LWP继承其父亲的时间片,
不管它是有限还是无限的。一个有有限时间片的LWP持续运行直到结束,阻塞(例如等
待一个I/O事件),被更高优先级的实时进程中断,或者时间片用完。一个拥有无限时
间片的进程则不存在第四种情况(即时间片用完)。
4.5.3 LWP调度和线程绑定
· 线程库自动调节缓冲池中LWP的数量来运行非绑定线程。其目标是:
避免程序因为缺少没有阻塞的LWP而阻塞。
例如,如果可运行的非绑定线程比LWP多而所有的活动线程在内核中处于无限等待
的阻塞状态,进程不能继续,知道一个等待的线程返回。
· 有效利用LWP
例如,如果线程库给每个线程创建一个LWP,许多LWP通常处于闲置状态,而操作
系统反而被没用的LWP耗尽资源。
要记住,LWP是按时间片运行的,而不是线程。这意味着如果只有一个LWP,则进程
内部没有时间片--现成运行直到阻塞(通过线程间同步),被中断,或者运行结束。
可以用thr_setprio(3T)来为线程分配优先级:只有在没有高优先级的非绑定线程
可用时,LWP才会被分配给低优先级的线程。当然,绑定线程不会参与这种竞争,因为
它们有自己的LWP。
把线程绑定到LWP上可以精确地控制调度。???但这种控制在很多非绑定线程竞
争一个LWP是不可能的。
实时线程可以对外部事件有更快的反应。考虑一个用于鼠标控制的线程,它必须对
鼠标事件及时作出反应。通过绑定一个线程到LWP上,保证了在需要时会有LWP可用。通
过将LWP设定为实时调度类型,可以保证LWP对LWP事件作出快速响应。
4.5.4信号等待(SIGWAITING)--给等待线程创建LWP
线程库通常保证在缓冲池内有足够的LWP保证程序运行。如果进程中所有的LWP处
于无限等待的阻塞状态(例如在读中断或网络时阻塞),操作系统给进程发送一个新的
信号,SIGWAITING。这个信号由线程库来控制。如果进程中有一个等待运行的线程,一
个新的LWP被创建并被赋予适当的线程使之执行。
SIGWAITING机制在一个或多个线程处于计算绑定并且有新线程可以执行的情况
下。
一个计算绑定线程可以阻止在缺少LWP的情况下有多个可运行的线程启动运行。这可以
通过调用thr_setconcurrency(3T)或者在调用thr_create(3T)时使用THR_NEW_LWP标志。
4.5.5确定LWP的已空闲时间
如果活动线程的数量减少,LWP池中的一些LWP将不再被需要。如果LWP的数量比活动
的线程多,线程库破坏那些没有用的LWP。线程库确定LWP的空闲的时间--如果线程在
足够长的时间内没有被使用(现在的设置是5分钟),它们将被删除。
4.6扩展传统的信号
为了适应多线程,UNIX的信号模型以一种相当自然的方式被扩展。信号的分布是用
传统机制建立在进程内部的(signal(2),sigaction(2), 等等)。
如果一个信号控制器被标志为SIG_DFL或者SIG_IGN,在收到信号后所采取的行动
(exit, core dump, stop, continue, or ignore)在整个接收进程中有效,将影响到
进程中的所有线程。关于信号的基本信息请参见signal(5)。
每个线程有它自己的信号掩模。如果线程使用的内存或其他状态也在被信号控制
器使用,线程会关于一些信号阻塞。进程中所有的线程共享由sigaction(2)和其变量
建立的信号控制器,???象通常那样。
进程中的一个线程不能给另一个进程中的线程发送信号。一个由kill(2)和sigsend
(2)送出的信号是在进程内部有效的,将被进程中的任何一个接收态的线程接收并处理。
非绑定线程不能使用交互的信号栈。一个绑定线程可以使用交互信号栈,因为其
状态是和执行资源连接的。一个交互信号栈必须通过sigaction(2) ,以及sigaltstack
(2)来声明并使能。
一个应用程序给每个进程一个信号控制器,在它的基础上,每个线程都有线程信
号控制器。一种办法是给在一张表中给每个线程控制器建立一个索引,由进程信号控制
器来通过这张表实现线程控制器。这里没有零线程。
信号被分为两类:陷阱(traps)和意外(exceptions,同步信号)和中断
(interrupts,异步信号)。
在传统的UNIX中,如果一个信号处于挂起状态(即等待接收),发生的其他同样的
信号将没有效果--挂起信号由一位来表示,而不是一个计数器。
就象在单线程的进程里那样,如果一个线程在关于系统调用阻塞时收到一个信号,
线程将提前返回,或者带有一个EINTR错误代码,或者带有比请求少的字节数(如果阻
塞在I/O状态)。
对于多线程编程有特殊意义的是作用在cond_wait(3T)上的信号的效果。这个调用
通常在其他线程调用cond_signal(3T)和cond_broadcast(3T),但是,如果等待线程收
到一个UNIX信号,将返回一个EINTR错误代码。更多的信息参见"对于条件变量的等待中
断"。
4.6.1同步信号
陷阱(例如SIGILL, SIGFPE, SIGSEGV)发生在线程自身的操作之后,例如除零
错误或者显式地发信号给自身。一个陷阱仅仅被导致它的线程类控制。进程中的几个
线程可以同时产生和控制同类陷阱。
扩展信号到独立线程的主张对于同步信号来说是容易的--信号被导致问题的线程
来处理。然而,如果一个线程没有处理这个问题,例如通过sigaction(2)建立一个信号
控制器,整个进程将终止。
因为一个同步信号通常意味着整个进程的严重错误,而不只是一个线程,终止进程
通常是一个明智的做法。
4.6.2异步信号
中断(例如SIGINT和SIGIO)是与任何线程异步的,它来自于进程外部的一些操作。
它们也许是显式地送到其他线程的信号,或者是例如Control-c的外部操作,处理异步
信号不处理同步信号要复杂的多。
一个中断被任何线程来处理,如果线程的信号掩模允许的话。如果有多个线程可以
接收中断,只有一个被选中。
如果并发的多个同样的信号被送到一个进程,每一个将被不同的线程处理,如果
线程的信号掩模允许的话。如果所有的线程都屏蔽该信号,则这些信号挂起,直到有信
号解除屏蔽来处理它们。
4.6.3连续语义(Continuation Semantics)
连续语义(Continuation Semantics)是处理信号的传统方法。其思想是当一个
信号控制器返回,控制恢复到中断前的状态。这非常适用于单线程进程的异步信号,如
同在示例4-1中的那样。在某些程序设计语言里(例如PL/1),这也被用于意外
(exception)处理机制。
Code Example 4-1 连续语义
Unsigned int nestcocunt;
Unsigned int A(int i, int j) {
Nestcount++;
If(i==0)
Return (j+1);
Else if (j==0)
Return (A(I-1,1));
Else
Return (A(I-1,A(I, j-1)));
}
void sig(int i){
printf("nestcount=%dn",nestcount);
}
main(){
sigset(SIGINT, sig);
A(4,4);
}
4.6.4对于信号的新操作
对于多线程编程的几个新的信号操作被加入操作系统。
Thr_sigsetmask(3T)
Thr_sigsetmask(3T)针对线程而sigprocmask(2)针对进程--它设置(线程)的
信号掩模。如果一个新线程被创建,它的初始信号掩模从父线程那里继承。
在多线程编程中避免使用sigprocmask(),因为它设置LWP的信号掩模,被这个
操作影响的线程可以在一段时间后改变。???
不象sigprocmask(),thr_sigsetmask()是一种代价相对低廉的调用,因为它不
产生系统调用。
Thr_kill(3T)
Thr_kill是kill(2)的线程版本--它发送信号给特定的线程。
当然,这与发送信号给进程不同。如果一个信号被发送给进程,信号可以被进
程中的任何线程所控制。一个由thr_kill()发出的信号只能被指定的线程处理。
注意,你只能用thr_kill()给当前进程里的线程发信号。这是因为线程标识符
是本地的--不可能给其他进程内的线程命名。
Sigwait(2)
Sigwait(2)导致调用线程阻塞直到收到set参数指定的所有信号。线程在等待时,
被set标识的信号应当被解除屏蔽,但最初的信号掩模在调用返回时将恢复。
用sigwait()来从异步信号当中把线程分开。你可以创建一个线程来监听异步信
号,而其它线程被创建来关于指定的异步信号阻塞。
如果信号被发送,sigwait()清除挂起的信号,返回一个数。许多线程可以同时
调用sigwait(),但每个信号被收到后只有相关的一个线程返回。
通过sigwait()你可以同时处理异步信号--一个线程通过简单的sigwait()调用
来处理信号,在信号一旦被受到就返回。如果保证所有的线程(包括调用sigwait()
的线程)屏蔽这样的信号,你可以保证这样的信号被你指定的线程安全地处理。
通常,用sigwait()创建一个或多个线程来等待信号。因为sigwait()可以接收
被屏蔽的信号,应当保证其它线程对这样的信号不感兴趣,以免信号被偶然地发送给
这样的线程。如果信号到达,一个线程从sigwait()返回,处理该信号,等待其它的
信号。处理信号的线程不限于使用异步安全函数,可以和其它线程以通常的方式同
步(异步安全函数类型被定义为"安全等级的MT界面MT Interface Safety Levels)。
---------------------------------------
注意-sigwait()不能用于同步信号
---------------------------------------
sigtimedwait(2)
sigtimedwait(2)类似于sigwait(2),不过如果在指定时间内没有收到信号,
它出错并返回。
4.6.5面向线程的信号(thread-directed signals)
UNIX信号机制扩展了一个叫做"线程引导信号"的概念。它们就象普通的异步信
号一样,只不过他们被送到指定线程,而不是进程。
在单独的线程内等待信号比安装一个信号控制器安全和容易。
处理异步信号的更好的办法是同时处理它们。通过调用sigwait(2),一个线程
可以等待一个信号发生。
Code Example 4-2 异步信号和sigwait(2)
Main(){
Sigset_t set;
Void runA(void);
Sigemptyset(&set);
Sigaddset(&set, SIGINT);
Thr_sigsetmask(SIG_BLOCK, &set, NULL);
Thr_create(NULL, 0, runA, NULL, THR_DETACHED, NULL);
While(1){
Sigwait(&set);
Printf("nestcount=%dn",nestcount);
}
}
void runA(){
A(4,4);
Exit(0);
}
这个例子改变了示例4-1:主函数屏蔽了SIGINT信号,创建了一个子线程来调
用前例中的函数A,然后用sigwait来处理SIGINT信号。
注意信号在计算线程中被屏蔽,因为计算线程继承了主线程的信号掩模。除非
用sigwait()阻塞,主线程不会接收SIGINT。
而且,注意在使用sigwait()中,系统调用不会被中断。
4.6.6完成语义(Completion Semantics)
处理信号的另外一种办法是用完成语义。完成语义使用在信号表明有极严重的
错误发生,以至于当前的代码块没有理由继续运行下去。该代码将被停止执行,取
而代之的是信号控制器。换句话说,信号控制器完成代码块。
在示例4-3中,有问题的块是if语句的then部分。调用setjmp(3C)在jbuf中保
存寄存器当前的状态并返回零--这样执行了块。
Code Example 4-3 完成语义
Sigjmp_buf jbuf;
Void mult_divide(void) {
Int a,b,c,d;
Void problem();
Sigset(SIGFPE, problem);
While(1) {
If (sigsetjmp(&jbuf) ==0) {
Printf("three numbers, please:n");
Scanf("%d %d %d", &a,&b,&c);
D=a*b/c;
Printf("%d*%d/%d=%dn",a,b,c,d);
}
}
}
void problem(int sig){
printf("couldn't deal with them,try againn");
siglongjmp(&jbuf,1);
}
如果SIGFPE(一个浮点意外)发生,信号控制器被唤醒。
信号控制器调用siglongjmp(3C),这个函数保存寄存器状态到jbuf,导致程序
从sigsetjmp()再次返回(保存的寄存器包含程序计数器和堆栈指针)。
然而,这一次,sigsetjmp(3C)返回siglongjmp()的第二个参数,是1。注意块
被跳过,在while循环的下一次重复才会执行。
注意,你可以在多线程编程中用sigsetjmp(3C)和siglongjmp(3C),但是要小心,
线程永远不会用另一个线程的sigsetjmp()的结果来做siglongjmp()。而且,
sigsetjmp()和siglongjmp()保存和恢复信号掩模,但sigjmp(3C)和longjmp(3C)
不会这样做。如果你使用信号控制器时最好使用sigsetjmp()和siglongjmp()。
完成语义经常用来处理意外。具体的,Ada语言使用这种模型。
--------------------------------------
注意-sigwait(2)永远不应用来同步信号。
4.6.7信号控制器和异步安全
有一个类似与线程安全的概念:异步安全。异步安全操作被保证不会和被中断
的操作相混。
如果信号控制器与正被中断的操作冲突,就会有异步安全的问题。例如,假设
有一个程序正在printf调用的当中,一个信号发生,它的控制器也要调用printf():
两个printf()的输出会交织在一起。为了避免这种结果,如果是printf被中断,控
制器就不应当调用printf。
这个问题使用同步原语无法解决,因为试图的同步操作会立即导致死锁。
例如,假设printf()用互斥锁来保护它自己。现在假设一个线程正在调用
printf(),第一个printf就得在互斥锁上等待,但是线程突然被信号中断了。如果
控制器(被在printf的里面中断的线程调用)也调用printf(),在互斥锁上阻塞的
线程回再次尝试得到printf的使用权,这就导致了死锁。
为了避免控制器和操作之间的干涉,或者保证这种情况永远不会发生(例如在
可能出问题的时刻封掉所有信号),或者在信号控制器中仅仅使用异步安全操作。
因为在用户级操作设置线程的掩模相对代价较小,你可以方便地设计代码使得
它符合异步安全的范畴。
4.6.8关于条件变量的中断等待
如果在线程等待条件变量的时候获得一个信号,过去的做法是(假设进程没有
终止)被中断的调用返回EINTR。
理想的新条件是当cond_wait(3T)和cond_timedwait(3T)返回,将重新获得互斥
锁。
Solaris多线程是这样做的:如果一个线程在cond_wait或cond_timedwait()函
数上阻塞,而且获得一个没有被屏蔽信号,(信号)控制器将被启用,cond_wait()
或cond_timedwait()返回EINTR,并且互斥锁加锁。???
这意味着互斥锁将被信号控制器获得,因为控制器必须清理环境。
请看示例4-4
Code Example 4-4 条件变量和等待中断
Int sig_catcher() {
Sigset_t set;
Void hdlr();
Mutex_lock(&mut);
Sigemptyset(&set);
Sigaddset(&set,SIGING);
Thr_sigsetmask(SIG_UNBLOCK,&set,0);
If(cond_wait(&cond,&mut) == EINTR){
/* signal occurred and lock is held */
cleanup();
mutex_unlock(&mut);
return(0);
}
normal_processing();
mutex_unlock(&mut);
return(1);
}
void hdlr() {
/* lock is held in the handler */
………
}
假设SIGINT信号在sig_catcher()的入口处被阻塞,而且hdlr()已被建立(通
过sigaction()调用)成为SIGINT的控制器。
如果线程阻塞在cond_wait()的时候,一个没有被屏蔽的信号被送给线程,线
程首先获得互斥锁,然后调用hdlr(),然后从cond_wait()返回EINTR。
注意,在sigaction()中指定SA_RESTART标志是没有效果的--cond_wait(3T)
不是系统调用,不会被自动重新启动。如果线程在cond_wait()阻塞时,调用总是
返回EINTR。
4.7 I/O事项
多线程的一个优势是它的I/O性能。传统的UNIX API在这一领域没有给程序员
足够的辅助--你或者使用文件系统的辅助,或者跳过整个文件系统。
这部分将介绍怎样在多线程利用I/O并发和多缓冲区来获得更多的灵活性。这
个部分也探讨了同步I/O(多线程)和异步I/O(可以是也可以不是多线程)的异同
。
4.7.1 I/O作为远程过程调用
在传统的UNIX模型里,I/O表现为同步的,就象你在通过一个远程过程调用
(RPC)来操纵外设。一旦调用返回,I/O完成(或至少看上去已完成--例如一个写
请求,也许仅仅是在操作系统内做数据移动)。
这个模型的优势在于容易理解,因为程序员对过程调用是很熟悉的。
一个代替的办法(在传统的UNIX里没有的)是异步模式,I/O请求仅仅启动一
个操作。程序要自己来发现操作是否完成。
这个办法不象同步方法那样简单,但它的优势在于允许并发的I/O处理和传统
的单线程进程处理。
4.7.2驯服的异步(Tamed Asynchrony)
你可以通过在多线程编程里使用同步I/O来获得异步I/O的大多数好处。在异
步I/O中,你发出一个请求,过一会儿再去检查请求是否已经完成,你可以用分离
的线程来同步操作I/O。然后由主线程(也许是thr_join(3T))检查操作是否完成
。
4.7.3异步I/O
在大多数情况下没有必要使用异步I/O,因为它的效果可以通过线程来实现,
每个线程使用同步I/O。然而,在少数情况下,线程不能完全实现实现异步I/O的功
能。
最直接的例子是用流的方法写磁带。这种技术在有持续的数据流写向磁带,磁
带驱动器高速运转时防止磁带驱动器停止。
为了作到这点,在磁带驱动程序响应一个标志上一个写操作已经完成的中断时,
内核里的磁带驱动器必须发出一个写请求队列。
线程不能保证异步写被排队,因为线程本身执行的顺序就是不确定的。例如试
图给磁带的写操作排队是不可能的。
*异步I/O操作
#include
int aioread(int filedes, char *bufp, int bufs, off_t offset,
int whence, aio_result_t *resultp);
int aiowrite(int filedes, const char *bufp, int bufs,
off_t offset, int whence, aio_result_t *resultp);
aio_result_t *aiowait(const struct timeval *timeout);
int aiocancel(aio_result_t *resultp);
aioread(3)和aiowrite(3)在形式上与pread(2)和pwrite(2),不同的是最后一
个参数。调用aioread()和aiowrite()导致初始化(或排队)一个I/O操作。
调用不会阻塞,调用的状态将返回到由resultp指向的结构。其类型为
aio_result_t,包含有:
int aio_return;
int aio_errno;
如果一个调用立即失败,错误码被返回到aio_errno。否则,这个域包含
AIO_INPROGRESS,意味着操作被成功排队。
你可以通过调用aiowait(3)来等待一个特定的异步I/O操作结束。它返回一个
指向aio_result_t数据结构的指针,该结构由最初的aioread(3)或者aiowrite(3)
提供。如果这些函数被调用,Aio_result包含类似与read(2)和write(2)相似返回
值,aio_errno包含错误代码,如果有的话。
Aiowait()使用一个timeout参数,该参数指定了调用者可以等多久。通常情况
下,一个NULL指针表示调用者希望等待的时间不确定,如果指针指向的数据结构包
含零值,表明调用者不希望等待。
你可以启动一个异步I/O操作,做一些工作,然后调用aiowait()来等待结束的
请求。或者你可以在操作结束后,用SIGIO来异步地通知。
最后,一个挂起的异步I/O操作可以通过调用aiocancel()来取消。这个过程在
调用时使用存放结果的地址做参数。这个结果区域标识了要取消哪一个操作。
4.7.4共享的I/O和新的I/O系统调用
如果多个线程同时使用同一个文件描述符来进行I/O操作,你会发现传统的
UNIX I/O接口不安全。在非串行的I/O(即并发)发生时会有问题。它使用
lseek(2)系统调用来为后续的read(2)和write(2)函数设置文件偏移量。如果两个或
更多的线程使用lseek(2)来移动同一个文件描述符,就会发生冲突。
为了避免冲突,使用新的pread(2)和pwrite(2)系统调用。
#include
#include
ssize_t pread(int fildes,void *buf,size_t nbyte,off_t offset);
ssize_t pwrite(int filedes,void *buf,size_t nbyte,off_t offset);
这些调用效果类似于read(2)和write(2),不同之处在于多了一个参数,文件
偏移量。用这个参数,你可以用不着用lseek(2)指定偏移量,多线程可以对同一个
文件描述符进行安全的操作。
4.7.5 Getc(3S)和putc(3S)的替代函数
一个问题会发生在标准I/O的情况下。程序员可以很快地习惯于getc(3S)和
putc(3S)这样的函数--它们是用宏来实现的。因为如此,他们可以在程序的循环内
部使用,用不着考虑效率。
然而,如果改用线程安全的版本后,代价会突然变的昂贵--它们需要(至少)
两个内部子程序调用,来给一个互斥锁加锁和解锁。为了解决这个问题,提供了这
些函数的替代版本--getc_unlocked(3S)和putc_unlocked(3S)。
这些函数不给互斥锁加锁,因此速度象非线程安全版本的getc(3S)和putc(3S)
一样快。然而如果按照线程安全的方法来使用的话,必须用flockfile(3S)和
funlockfile(3S)显式地给互斥锁加锁和解锁来保护标准的I/O流。这两个调用放在
循环外面,而getc_unlocked()或者putc_unlocked()放在循环内部。
第五章 安全和不安全的接口
本章定义了函数和库的多线程安全等级。
线程安全
多线程接口安全等级
异步安全函数
库的多线程安全等级
5.1线程安全
线程安全是为了避免数据竞争--数据设置的正确性依赖于多个线程修改数据
的顺序。
如果不需要共享,则给每个线程分配一个私有的数据拷贝。如果数据必须共
享,一定要用同步机制来保证操作的唯一性。
如果一个线程在几个线程同时执行时在逻辑上是正确的,则称它为线程安全
的。在一个实际的水平上,把安全等级划分为3层比较方便。
· 不安全
· 线程安全--非并行
· 线程安全--多线程安全
一个不安全的过程可以用在操作前加互斥锁,操作后解互斥锁的办法来使操
作序列化(即消除并发)。示例5-1首先显示了一个简化的fputs()的非线程安全
实现。
接下来是用单互斥锁保护使操作序列化的版本。实际上,使用了比需要的更
强的同步。如果两个线程调用fputs()来打印到不同的文件时,其中一个用不着
等待另一个--它们可以同时操作。
最后一个版本是多线程安全版。它给每个文件加一个锁,允许两个线程同时
指向不同的文件。所以,MT-SAFE(即多线程安全)的函数是线程安全的,并不会使
运行性能变坏。
Code Example 5-1 线程安全的程度
/*not thread-safe */
fputs(const char *s, FILE *stream){
char *p;
for(p=s; *p; p++)
putc((int)*p,stream);
}
/*serializable*/
fputs(const char *s,FILE *stream){
static mutex_t mut;
char *p;
mutex_lock(&m);
for(p=s;*p;p++)
putc((int)*p,stream);
mutex_unlock(&m);
}
/*MT-SAFE*/
mutex_t m[NFILE];
fputs(const char *s, FILE *stream){
static mutex_t mut;
char *p;
mutex_lock(&m[fileno(stream)]);
for (p=s;*p;p++)
putc((int)*p,stream);
mutex_unlock(&m[fileno(stream)]);
}
5.2多线程接口安全等级
man page(3):库函数用下面的分类来描述一个接口支持多线程到什么程度
(这些分类在Intro(3) man page中解释地更为详细)。
Safe 可以被多线程应用程序调用
Safe with exceptions 例外的部分请参见NOTES部分
Unsafe 这个接口只有在应用程序保证一个时刻只有一个线程执行时才
能安全调用
MT-Safe 完全为多线程设计,不但安全,还支持一些并发性
MT-Safe with exceptions 例外的部分请参见NOTES部分
Async-Safe 可以被一个信号控制器安全调用。一个线程在执行
Async-Safe函数时被信号中断将不会产生死锁。
有关safe接口请看附录B的表"MT Safety Levels:Library Interfaces.",
它来自man pages(3)。如果一个第三部分的接口不在表内,它就有可能是不
安全的(不包括源兼容库Source Compatibility Library)。检查man page后才
能确定。
在"man pages(2):系统调用"中描述的所有函数,除了vfork(2)外都是
MT-Safe的。
一些函数有意地不作成安全,因为如下原因。
对于单线程的应用程序,MT-Safe回在一定程度上降低性能。
函数本身有一个不安全接口。例如,一个函数会返回一个指向堆栈缓冲区
的指针。你可以用这些函数"再进入"的对等函数???(原文为
reentrant counterparts)。再进入函数的名字是原函数加"_r"后缀。
-------------------------------------
注意--除非通过查询手册页(man pages),否则无法确定一个不以"_r"结尾的
函数是否MT-safe。非MT-safe的函数一定要有同步机制的保护,或者被限制在
初始线程里。
------------------------------------
*非安全接口的替代(重入 Reentrant)函数
对于大多数非安全接口的函数,都存在一个MT-safe的版本。新的MT-safe函
数一般是旧的非安全函数加上"_r"后缀。Solaris系统提供以下的"_r"函数。
Table 5-1 替代函数
asctime_r(3C) ctermid_r(3S) ctime_r(3C)
fgetgrent_r(3C) fgetpwent_r(3C) fgetspent_r(3C)
Gamma_r(3M) getgrgid_r(3C) getgrnam_r(3C)
getlogin_r(3C) getpwnam_r(3C) getpwuid_r(3C)
getgrent_r(3C) gethostbyaddr_r(3N) gethostbyname_r(3N)
gethostent_r(3N) getnetbyaddr_r(3N) getnetbyname_r(3N)
getnetent_r(3N) Getprotobyname_r(3N) getprotobynumber_r(3N)
getprotoent_r(3N) getpwent_r(3C) getrpcbyname_r(3N)
getrpcbynumber_r(3N) getrpcent_r(3N) getservbyname_r(3N)
getservbyport_r(3N) getservent_r(3N) getspent_r(3C)
getspnam_r(3C) gmtime_r(3C) lgamma_r(3M)
localtime_(3C)r nis_sperror_r(3N) rand_r(3C)
readdir_r(3C) strtok_r(3C) tmpnam_r(3C)
ttyname_r(3C)
5.3异步安全函数
可以被信号控制器安全调用的函数被称为Async-Safe的。POSIX标准定义并
详列了异步安全函数(IEEE Std 1003.1-1990.3.3.1.3(3)(f), page 55)。除
了POSIX异步安全函数外,下列三个函数也是异步安全的。
· sema_post(3T)
· thr_sigsetmask(3T)
· thr_kill(3T)
5.4库的多线程安全等级
所有可能被多线程程序的线程调用的函数都应当是MT-Safe的。
这意味着过程可以同时正确地执行两个操作。所以,每一个被多线程程序
使用的接口都应是MT-Safe。
并不是所有的库都是MT-Safe的。通常被使用的MT-Safe的库详列于表5-2中。
其他的库也将最终被改写成MT-Safe的。
表5-2 一些MT-Safe库
------------------------------------
库 说明
------------------------------------
lib/libc getXXbyYY接口(例如gethostbyname(3N))是MT-Safe的
lib/libdl_stubs (支持static switch compiling)
lib/libintl
lib/libm 仅当为共享库编译时是MT-Safe的,但与文档库连接时
不是MT-Safe的
lib/libmalloc
lib/libmapmalloc
lib/libnsl 包括TLI接口,XDR,RPC客户方和服务方,netdir和
netselect。 GetXXbyYY是不安全的,但有线程安全版本
GetXXbyYY_r
lib/libresolv 支持因线程而异的错误码
lib/libsocket
lib/libw
lib/nametoaddr
lib/nametoaddr
lib/nsswitch
libX11
libC (不是Solaris系统的部分;可以分开购买)
------------------------------------
*不安全库
如果库中的函数不是MT-Safe的,则只有在一个线程的调用时才是安全的。
6 编译和调试
本章描述了怎样编译和调试多线程程序。
编译一个多线程应用程序
调试一个多线程应用程序
6.1编译一个多线程应用程序
6.1.1使用C编译器
确认你拥有如下软件,否则将无法正常编译和连接多线程程序
· 头文件:thread.h errno.h
· 标准C编译器
· 标准Solaris连接器
· 线程库(libthread)
· MT-Safe库(libc, libm, libw, libintl, libmalloc,
libmapmalloc, libnsl, 等等)
6.1.2用替代(_REENTRANT)标志进行编译
在编译多线程程序时使用"-D _REENTRANT"标志。
这个标志必须在编译应用程序的每一个模块时都使用。如果没有这个标志,将
使用errno, stdio等等的旧的定义。如果要编译一个单线程应用程序,不要使用这
个标志。
*新旧连接需要小心
表6-1显示了多线程目标代码模块与旧的代码模块连接时需要非常慎重。
表6-1 在编译多线程程序时使用"-D _REENTRANT"标志
文件类型 编译 参考 返回
----------------------------------------------------------------------
旧的目标文件(非线程版)
和新的目标文件 没有 "- 静态 储存 传统的errno
D _REENTRANT" 标志。
----------------------------------------------------------------------
新的目标文件 有
"-D _REENTRANT"标志。 __errno,新的二进制入口
----------------------------------------------------------------------
线程 定义errno的地址
----------------------------------------------------------------------
用libnsl 里的TLI编程 有 "
-D _REENTRANT"标志(必须)。 __t_errno,一个新的入口 线程定义t_errno的地址
----------------------------------------------------------------------
6.1.3使用libthread
为了在连接时使用libthread,需要在ld命令行里,-lc参数之前,指定
-lthread,或者在cc 命令行的最后指定。
如果应用程序没有连接libthread,则对该库中的函数调用不产生实际操作。
Libc定义libthread为空过程。???真正的过程是在应用程序既连接libc也
连接libthread时由libthread加入的。
如果一个ld命令行包含了以下的字段:.o's ... -lc -lthread ...,则C函数
库的行为没有被定义。???
不要在单线程程序中使用-lthread。这样做将在连接时建立多线程机制,在运
行时将被初始化。这样做不但浪费资源,而且在调试中会对运行结果有不正确的显
示。
6.1.4使用非C的编译器
线程库使用libc中的如下内容:
· 系统调用包装器(system call wrappers)
· 用来显示出错信息的调用(通常是printf)
· 运行时的连接支持来解析符号(因为库是动态连接的)
你也可以写自己的系统调用包装器和自己的printf函数,并且在连接时(而不
是在运行时)进行符号解析,这样可以消除对libc的依赖。
如果线程使用应用程序提供的堆栈,则线程库不使用动态分配内存的办法。
Thr_create(3T)函数可以由应用程序指定自己的堆栈。
6.2调试多线程应用程序
6.2.1一般的疏漏
以下列出可以导致多线程出错的常见疏漏:
· 给新线程传递参数时使用局部或全局变量
· 在没有同步机制的保护下访问全局内存
· 两个线程以不同的顺序去申请两个资源导致死锁(两个线程各自占有一个资源
并相执不下)
· 在同步保护中有隐藏的漏洞。例如可能有如下情况:一个有同步机制(例如互斥
锁)保护的代码段包含一个先释放再重新获得同步机制的函数调用,结果是全局内存
实际上没有得到保护。
· 有隐匿的,重复或递归的大自动数组的使用可能导致问题,因为多线程程序的堆
栈容量比单线程程序有更多的限制。
· 指定的堆栈空间不够。
· 没有通过线程库的调用指定堆栈。
注意,多线程程序(特别是有错误的)经常在相同输入的情况下得到不同的结
果,因为线程调度的顺序不同。
一般的,多线程bug具有统计性,而不是确定性。在调试时,跟踪的办法将会比
设断点的办法好些。
6.2.2使用adb
如果你在一个多线程程序当中绑定所有线程,一个线程和一个LWP是同步的。
然
后你通过如下支持多线程编程的adb命令访问每一个线程。
表6-2 MT adb命令
-------------------------------------
pid:A 绑定在进程pid上,这将停止进程及其所有LWP
:R 与进程分离,这将恢复进程及其所有LWP
$L 显示在(停止的)进程中所有的活动的LWP
n:l 将焦点切换到第n号LWP
$l 显示当前焦点所在的LWP
num:i 忽略信号码为num的信号
6.2.3使用dbx
使用dbx,可以调试和执行用C++, ANSI C, FORTRAN和PASCAL的源程序。Dbx使
用与SPARCworks? Debugger相同的命令,但使用标准终端(tty)接口。Dbx和
SPARCworks Debugger现在都支持多线程程序。
要得到dbx和Debugger的全面认识,请参考SunPro dbx(1) man page和
《Debugging a Program》用户指南。
以下的dbx选项支持多线程。
表6-3 给MT程序使用的dbx选项
Cont at line[sig signo id] 在信号signo发生时继续执行第line行。
参见dbx的命令语言的循环控制里的continue。
如果有id参数,则指定继续哪一个线程或LWP。
缺省设置为all。
Lwp 显示当前LWP。切换到给定LWP[lwpid]
Lwps 列出当前进程的所有LWP
Next … tid 单步执行指定线程。如果一个函数调用被跳过,
所有的LWP在该函数调用期间重新开始???非
活动线程不能被单步执行
Next … lid 单步执行指定LWP。在跳过函数时并不隐含地恢
复所有的LWP。在该LWP上的线程是活动的。
Step … tid 单步执行指定线程。如果一个函数调用被跳过,
所有的LWP在该函数调用期间重新开始???非
活动线程不能被单步执行
Step … lid 单步执行指定LWP。在跳过函数时并不隐含地恢
复所有的LWP。
Stepi … lid 指定的LWP
Stepi … tid 在LWP上的线程是活动的。
Thread 显示当前线程。切换到线程tid。在以下情况中,
一个可选的tid指当前线程。
Thread -info[tid] 打印指定线程的所有已知情况。
Thread -locks[tid] 打印被指定线程控制的所有锁
Thread -suspend[tid] 把指定线程置于挂起状态。
Thread -continue[tid] 使指定线程退出挂起状态。
Thread -hide[tid] 隐藏指定(或当前)线程,在普通线程列表中
将不被显示出来
Thread -unhide [tid] 解除指定线程的隐藏状态
Allthread-unhide 解除所有线程的隐藏状态
Threads 打印已知线程的列表
Threads-all 打印所有线程(包括通常不被打印的,zombies)
All|filterthreads-mode 控制threads命令打印所有线程还是有选择地列表
Auto|manualthreads-mode 使在GUI界面里线程监控器(Thread Inspector)
线程列表得以自动更新
Threads -mode 显示当前模式。Any of the previous
forms
can be followed by a
thread or LWP ID to get the traceback for the specified entity.
☆☆风起的日子笑看落花☆☆☆
7 编程指南
本章给出线程编程的一些要点。特别强调了单线程和多线程编程方法的差别。
重新认识全局变量
提供静态局部变量
线程同步
避免死锁
一些基本的注意事项
用多处理器编程
在历史上,大多数代码以单线程的方式来编程。如果在C程序里调用库函数则
尤其是这样:
· 如果你给全局变量赋值,并且在一会以后读该变量,则读的结果和写的是一样的。
· 对于非全局的,静态存储也是如此
· 不需要同步机制,因为没有什么可以同步的
在下面的几个多线程例子当中讨论了采用以上假设将会发生的问题,以及你
如何处理这些问题。
7.1重新认识全局变量
传统情况下,单线程C和UNIX有处理系统调用错误的传统。系统调用可以返回
任何值(例如,write()返回传输的字节数)作为功能性的返回值。然而,-1被保留,
它意味着出错。所以,如果一个系统调用返回-1,你就知道是失败了。
Code Example 7-1 全局变量和错误码errno
Extern int errno;
…
if(write(file_desc,buffer,size)==-1){
/* the system call failed */
fprintf(stderr,"something went wrong, error code = %dn",
errno);
exit(1);
}
…
函数并不直接返回错误码(将会和正常的返回值混淆),而是将错误码放入一个
名为errno的全局变量中。如果一个系统调用失败,你可以读errno来确定问题所在。
现在考虑在多线程程序中,两个线程几乎同时失败,但错误码不同。他们都希望
在errno中寻找问题,但一个errno不能存放两个值。这个全局变量不能被多线程程序
使用。
Solaris线程包通过一种在概念上全新的存储类型解决了这个问题--线程专有数据。
与全局变量类似,在线程运行时任何过程都可以访问这块内存。然而,它是线程私有
的--如果两个线程参考同名的线程专有存储区,它们实际上是两个存储区。
所以,如果使用线程,每个对errno的操作是线程专有的,因为每个线程有一个
errno的私有拷贝。
7.2提供给静态局部变量
示例7-2显示了一个类似与errno错误的问题,但涉及到静态存储,而不是全局存
储。Gethostbyname(3N)函数用计算机名称作为参数。返回值是一个指向结构的指针,
该结构包含通过网络访问指定计算机的必要信息。
Code Example 7-2 gethostbyname问题
Struct hostent *gethostbyname(char *name){
Static struct hostent result;
/*lookup name in hosts database */
/*put answer in reault*/
return(&result);
}
返回指向自动局部变量不是一个好的选择,尽管在这个例子是可以的,因为所指
定的变量是静态的。但是,如果两个线程用不同的计算机名同时访问这个区域,对静
态存储的使用就会发生冲突。
线程专有数据可以代替静态储存,就象在errno问题中那样,但是这涉及到动态
分配内存,并且增加了调用的开销。
一个更好的办法是调用者提供存放数据的存储区。这需要在函数调用中增加一个
参数,一个输出参数。即需要一个gethostbyname的新的版本。
在Solaris里,这种技术被用来处理很多类似问题。在大多数情况下,新接口的
名字都带有"_r"后缀,例如gethostbyname_r(3N)。
7.3线程同步
应用程序中的线程在处理共享数据和进程资源是必须使用同步机制。
在多个线程控制一个对象时会出现一个问题。在单线程世界里,对这些对象的同
步访问不是问题,但正如示例7-3所示,在多线程编程中需要注意。(注意
Solaris
printf(3S)对多线程程序是安全的;此例说明如果printf不安全将会发生的问题
。)
Code Example 7-3 printf()问题
/*thread 1*/
printf("go to statement reached");
/*thread 2*/
printf("hello world");
printed on display:
go to hello
7.3.1单线程策略
一个办法是采用单一的,应用程序范围有效的互斥锁,在调用printf时必须使用
互斥锁保护。因为每次只有一个线程可以访问共享数据,每个线程所见的内存是一致
的。
Because this is effectively a single-threaded program, very little is
gained bythis strategy.
7.3.2重入(reentrant)函数
更好的办法是采用模块化和数据封装的思想。一个替代函数被提供,在被几个线
程同时调用时是安全的。写一个替代函数的关键是搞清楚什么样的操作是"正确的"。
可以被几个线程调用的函数一定是重入的。这也许需要改变函数接口的实现。
访问全局状态的函数,例如内存和文件,都存在重入问题。这些函数需要用
Solaris线程提供的正确的同步机制来保护自己访问全局状态。
两个保证函数重入的基本策略是代码锁和数据锁。
7.3.2.1代码锁
代码锁是函数调用级的策略,它保证函数完全在锁的保护下运行。该策略假设对
数据的所有访问都是通过函数。共享数据的函数应当在同一个锁的保护下执行。
有些并行编程语言提供一种名为监视器(monitor)的机制,在monitor的内部,
函数的代码被隐含地用所来保护。一个monitor也可以用互斥锁实现
7.3.2.2数据锁
数据锁保证对数据集合(collection of data)维护的一致性。对于数据锁,仍
然有代码锁的概念,但代码锁是仅仅围绕访问共享数据进行。对于一个互斥锁协议,
仅有一个线程来操作每一个数据集合。???
在多读单写协议当中,几个读操作或一个写操作可以被允许。在操作不同的数据
集合,或者在同一个数据集合上不违反多读单写的协议的前提下,一个模块中的多个
线程可以同时执行。所以,数据锁比代码锁提供了更多的同时性。
如果你需要使用锁的话,你要用哪一种(互斥锁,条件变量,信号量)呢?你需
要尝试只在必要时加锁来允许更多的并发呢(fine-grained locking 细纹锁),还
是使锁在相当一段时间内有效来避免加锁和释放锁的额外开销呢(coarse-grained
locking 粗纹锁)?
锁的纹理(可以理解成加锁和释放锁的频率,频率越高则纹理越细--译者注)依
赖于所保护的数据量。一个粗纹锁可以是一个保护所有数据的单一的锁。把数据由适
当数量的锁分开来保护是很重要的。如果纹理过细可能会影响性能,过多的加锁和解
锁操作会累计到相当的程度。
普通的做法是:用一个粗纹锁开始,找到限制性能的瓶颈,然后在需要时加入细
纹锁来减缓瓶颈。看上去这是一个合理的办法,但你需要自己判断来达到最好效果。
7.3.2.3不变量
不论是代码锁还是数据锁,不变量对于控制锁的复杂度都具有重要的意义。一个
不变量是一个永真的条件或关系。
这个定义在应用在同时执行时需要进行一定的修改:一个不变量是一个永真的条
件或关系,如果相关的锁尚未设置。一旦锁被设置,不变量就可能为假。然而,拥有
锁的代码在释放所前一定要重新建立不变量。
一个不变量也可以是永真的条件或关系,如果锁尚未设置。条件变量可以被认为
拥有一个不变量,那就是它的条件。
Code Example7-4 用assert(3X)来测试不变量
mutex_lock(&lock);
while(condition)
cond_wait(&cv);
assert((condition)==TRUE);
.
.
.
mutex_unlock();
Assert()命令是测试不变量的。Cond_wait()函数不保护不变量,所以线程返回
时一定要重新估价不变量。
另外一个例子是一个控制双链表元素的模块。对链表中每一个组件,一个好的不
变量是指向前项的指针,以及指向其后项的指针。
假设这个模块使用代码锁,即仅仅用一个全局互斥锁进行保护。如果某一项被删
除或者某一项被添加,将会对指针进行正确操作,然后释放互斥锁。显然,在操作指
针的某种意义上不变量为假,但在互斥锁被释放之前不变量会被重新建立。
7.4避免死锁
死锁是一系列线程竞争一系列资源产生的永久阻塞。某些线程可以运行并不说明
其它线程没有死锁。
导致死锁的最常见的错误是自死锁(self deadlock)或者递归死锁(recursive
deadlock):一个线程在拥有一个锁的情况下试图再次获得该锁。递归死锁是编程
时非常容易发生的错误。
例如,如果一个代码监视器在调用期间让每一个模块的函数都去获得互斥锁,那
么任何在被互斥锁保护的模块之间调用的函数都将立即导致死锁。如果一个函数调用
模块以外的一些代码,而这些代码通过一个复杂或简单的路径,又反过来调用该模块
内部被同一互斥锁保护的函数,也会发生死锁。
解决这种死锁的办法是避免调用模块以外的函数,如果你并不知道它们是否会在
不重建不变量的情况下回调本模块并且在调用之前丢弃所有已获得的锁。当然,在调
用完成后锁会重新获得,一定要检查状态以确定想要进行的操作仍然合法。
死锁的另外一种情况是,线程1和线程2分别获得互斥锁A和互斥锁B。这时线程1
想获得互斥锁B,而同时线程2想获得互斥锁A。结果,线程1阻塞等待B,而线程2阻塞
等待A,造成死锁。
这类死锁可以通过为互斥锁编排顺序来避免(锁的等级 lock hierarchy)。如
果所有线程通过指定顺序申请互斥锁,死锁就不会发生。
为锁定义顺序并非最优的做法。如果线程2在拥有互斥锁B时对于模块的状态有很
多的假设,则放弃互斥锁B来申请互斥锁A,然后再按照顺序重新申请互斥锁B将导致
这些假设失去意义,而不得不重新估价模块的状态。
阻塞同步原语通常有不阻塞的版本,例如mutex_trylock()。它允许线程在没有
竞争时打破锁等级。如果有竞争,已经获得的锁通常要释放,然后按照顺序来申请。
7.4.1死锁调度
因为锁的获得没有顺序的保证,一个线程编程的普遍问题是一个特定线程永远不
会得到一个锁(通常是条件变量),即使它看上去应当获得。
这通常发生在拥有互斥锁的线程释放了锁,在一段时间之后又重新获得了这个锁。
因为锁被释放了,似乎其他线程会获得这个锁。但是因为没有谁能阻塞这个已经获得
了锁的线程,它就继续执行到重新获得互斥锁的时候,这样其他线程无法进行。
通常可以用在重新获得锁之前调用thr_yield(3T)来解决这一类型的问题。它允
许其它线程运行并获得锁。
因为应用程序需要的时间片变化很大,线程库不能做强制规定。只有调用
thr_yield()来保证线程按你需要的那样共享资源。
7.4.2加锁的注意事项
以下是有关锁的一些简单的注意事项。
· 在长时间的操作(例如I/O)上尽量不要加锁,这样会对性能造成负影响。
· 在调用可能重新进入本模块的函数时不要加锁。
· 不要尝试极端的处理器同时性。在不涉及系统调用和I/O操作的情况下,锁通常只
被线程占有很短的时间,冲突是很少发生的。只有在确知竞争情况时,才能长时间占
有一个锁。
· 如果使用多个锁,用锁等级来防止死锁。
7.5遵循基本的注意事项
· 搞清你引入的内容以及它们是否安全。
一个线程程序不能任意进入非线程代码。
· 只有在初始线程中线程代码才可以调用非安全代码。
这保证了与初始线程关联的静态存储只能被该线程使用。
· Sun提供的库如果没有明确地标识为unsafe,则被定义为safe。
如果man page不声称函数是MT-Safe的,则它是安全的。所有的MT-unsafe函
数都在man page里明确标出。
· 使用编译标志来控制二进制不兼容的源代码改变。
在编译时指定-D_REENTRANT或者保证在头文件中定义_REENTRANT。
· 如果一个库是多线程安全的,不要将全局进程操作线程化。???
不要把全局操作(或者有可能影响全局的操作)改成线程风格。例如,如果
文件的I/O操作被设置为线程级的操作,多个线程将不能正确访问文件。
对于线程级的操作,或者thread cognizant的操作,要使用线程工具。例如,如
果main()函数仅仅终止正在退出main函数的那个线程,则main()函数的结尾应当为
thr_exit();
/*not reached */
7.5.1创建线程
Solaris线程包对线程数据结构,堆栈以及LWP设置缓存,这样重复创建非绑定线
程开销会降低。
非绑定线程的创建比其进程创建或者绑定线程的创建来开销都要小的多。实际上,
这种开销和从一个线程切换到另外一个线程的开销是相当的。
所以,在需要时不断地创建和清除线程,比起维护一个等待独立任务的线程池通
常要划算一些。
一个好的例子是,一个RPC服务器的工作方式是为监听到的每一个请求创建一个
线程,并且在提供服务后清除这个线程,而不是维护很多线程来提供服务。
虽然线程创建比进程创建开销要小,但是比起几条指令来代价并不低。因此仅在
需要执行几千条以上机器指令时才考虑创建线程。
7.5.2线程同时性
在缺省状态下,Solaris线程通过对非绑定线程调整执行资源(LWP)来实现对实
际活动的线程数的匹配。如果说Solaris线程包不能进行完美的调度,它至少可以保证
进程继续运行。
如果你需要让一定数量的线程同时处于活动状态(执行代码或系统调用),需要
通过thr_setconcurrency(3T)来通知线程库。
例如:
· 如果一个数据库服务器给每一个用户开设一个服务线程的话,它应当把期望得到的
用户数目告诉操作系统Solaris。
· 如果一个窗口服务器给每一个客户开设一个线程,它应当把期望的活动客户端的
数目通知Solaris。
· 一个文件拷贝程序拥有一个读线程和一个线程,它应当通知Solaris它的同时性等级
为2。
或者,同时性等级可以在创建线程时使用THR_NEW_LWP标志来增加。
在计算线程的同时性时,需要把因为进程间的同步变量而处于阻塞状态的线程考虑
进来。
7.5.3效率
用thr_create(3T)创建一个新线程比重新启动一个线程花费的时间少。这意味
着,在需要时创建一个线程并且在任务结束后用thr_exit(3T)立刻杀掉它,比起维护一
大堆的空闲线程并且在它们中间切换要划算的多。
7.5.4绑定线程
绑定线程比起非绑定线程的开销要大。因为绑定线程可以改变它所在的LWP的属性,
LWP在绑定线程退出后不会被缓存,在新的绑定线程生成时,操作系统将提供一个新的
LWP。
仅仅在线程需要只有在所在的LWP内可用的资源时(例如虚拟的定时器或者一个指定
的堆栈),或者为了实现实时调度而必须使线程对于内核可见的场合下,才需要使用绑
定线程。
即使在你希望所有的线程都同时活动时,你也应当使用非绑定线程。因为非绑定线程
允许Solaris高效率地分配系统资源。
7.5.5线程创建指南
在使用线程时有如下简单的注意事项:
· 在有多个执行大量任务的操作时,使用多线程编程。
· 使用线程来更好地利用CPU的同时性。
· 只有在不得已的情况下再使用绑定线程,就是说,需要LWP的特殊支持的时候。
使用thr_setconcurrency(3T)来告诉Solaris你希望有多少个线程同时执行。
7.6关于多处理器
Solaris线程包使你能够充分利用多处理器。在很多情况下,程序员必须关心程序
是在单处理器还是在多处理器的环境下运行。
这样的情况下涉及到多处理器的内存模型。你不能够假设一个处理器对内存所做的
改变可以被另一个处理器立即看到。
另一个与多处理器有关的问题是如何实现"多个线程在到达同一点后再向下执行"的
有效同步。
--------------------------------------
注意-如果同步原语已经被应用于共享的内存,这里讨论的问题将不重要。
--------------------------------------
7.6.1基本建构
如果多个线程对共享存储区的访问使用的是Solaris的线程同步函数,那么程序在
多处理器的环境和单处理器的环境下的运行效果是一样的。
然而,在很多情况下,有些程序员希望更充分地发挥多处理器的优势,希望使用一
些"巧妙"的办法避开线程同步函数。如示例7-5和7-6所示,这样的办法是危险的。
了解一般的多处理器结构支持的内存模型有助于了解这种危险。
主要的多处理器组件是:
处理器本身
CPU缓冲区(Store buffers),它连接处理器和其高速缓存(caches)
高速缓存(caches),保存最近访问和修改过的存储地址
内存(memory),主要存储器,被所有的处理器共享
在简单的传统模型里,多处理器的操作就象是直接与内存打交道:一个处理器A
向一个内存单元写数后,另一个处理器B立刻读取该单元,取出的数一定是处理器A刚
刚写入的。高速缓存可以被用来加快平均的内存访问速度,如果高速缓存之间保持一
致的话,的确可以达到期望的效果。
这种简单方法的一个问题在于,处理器必须有一定的延迟来保证期望的语义效果
实现。许多新的多处理器结构使用各种办法来减少这种延迟,结果不得不改变了内存
模型的语义。在如下的两个例子当中,我们会解释两个这种技术和它们的效果。
7.6.1. 1"共享内存"的多处理器系统
考虑示例7-5的生产者/消费者解决方案。尽管这个程序在现在的SPARC多处理器
系统上是可行的,但它假定了所有的多处理器系统都有高度有序的内存,因而是不
可移植的。
示例7-5 生产者/消费者问题--共享内存的多处理器
char buffer[size];
unsigned int in=0;
unsigned int out=0;
void producer(char item){
do
;/*nothing*/
while
(in - out == BSIZE);
buffer[in%BSIZE] = item;
in++;
}
char consumer(void){
char item;
do
;/*nothing*/
while
(in - out == 0);
item = buffer[out%BSIZE];
out ++;
}
如果这个程序仅有一个生产者和一个消费者,并且运行在一个共享内存的多
处理器系统当中,它似乎是正确的。In和out的差别是缓冲区中的产品数。生产者
等待直到有空位置,消费者等待直到缓冲区中有产品。
对于高度有序的内存(例如,一个处理器对内存的改变立刻对另一个处理器
生效),这种解决是正确的(即使考虑到in和out将最终溢出,程序仍然是正确的,
因为BSIZE比word型数据能表示的最大整数要小)。
共享内存的多处理器系统不一定拥有高度有序的内存。一个处理器对于内存
的改变未必会立刻通知其他处理器。如果一个处理器改变了两处内存,其他处理
器不一定看到两处改变按照预期的顺序发生,因为对内存的改变不是立即执行的。
写操作首先保存在CPU缓冲区里,对高速缓存是不可见的。处理器自己对这些缓冲
数据的维护是可靠的,但它对其它的处理器不可见,所以在数据写到高速缓存之
前,其他处理器认为该写操作没有发生。
Solaris同步原语(见第三章)使用特殊的指令将数据从CPU缓冲区写入高速
缓存。这样,在共享数据的访问前后加上锁的保护,就可以保证内存的一致性。
如果内存的顺序保护非常松散,在示例7-5中,消费者看到in变量被生产者
增加时,也许产品item还没有放入产品缓冲区。这种情况被称为weak ordering
(弱顺序),因为一个处理器的操作在另一个处理器看来是打乱次序的(但内存对
同一个处理器总是保持一致)。解决这个问题的办法是使用互斥锁来强制更新高
速缓存。
现在的处理器趋向于"弱顺序"。因此,程序员必须在操作全局或共享内存时
使用锁。象在示例7-5和7-6中那样,锁是必不可少的。
7.6.1.2彼得森算法(Peterson's Algorithm)
示例7-6是Peterson's Algorithm的一个实现,它控制两个线程的互斥。这段
代码试图保证一个时间最多只有一个线程在执行关键代码,进而当一个线程调用
mut_excl()时,它在很"近"的一个时刻进入该关键代码。
假定线程进入关键代码后很快退出。
示例7-6 两个线程的互斥?
Void mut_excl(int me /*0 or 1*/){
Static int loser;
Static int interested[2]={0,0};
Int other;/* local variable */
Other = 1-me;
Interested[me]=1;
Loser=me;
While (loser == me && interested[other]);
/* critical section */
interested[me];
}
这个算法在多处理器有高度有序的内存时,可能是可以正确运行的。
一些多处理器系统,包括一些SPARC系统,都有CPU缓冲区。如果一个线程发出
一条存储指令,数据被放入CPU缓冲。这些数据最终都被送往高速缓存,但不是立刻
(注意高速缓存对其它的处理器来说是可见的,一致的,但数据并非立刻写入高速
缓存)。
如果同时写入多个内存地址,这些改变将按顺序到达高速缓存和内存,但有一
定延迟。拥有这种属性的SPARC多处理器系统被称为全存储顺序
(TSO:Total Store Order)。
如果某个时间一个处理器向A地址写,然后读取B地址,而另一个处理器向B地址
写,然后读取A地址,其预期结果是或者第一个处理器得到B的新值,或者第二个处
理器得到B的新值,或者二者都得到,但不会发生两个处理器都得到旧值的情形。
但是,由于从CPU缓冲区存取的延迟存在,这种不可能的事情就会发生。
在Peterson's Algorithm算法中可能发生的是:两个线程分别在两个处理器上
运行,数据都保存在CPU缓冲区中,然后读取另外一个,他们看到的都是旧的值(
0),
表示另外的线程当前不在关键代码中,所以他们共同进入关键代码(注意,这种问
题在你测试的时候可能不会表示出来,但它是可能发生的)。
如果你用线程同步原语,就可以避免这个问题。同步原语强制地将CPU缓冲区
的数据写入高速缓存。
7.6.1.3在共享内存的并行计算机里并行一个循环
在很多应用程序中,特别是数值计算,算法的某些部分可以并行,而另外一
些部分必须顺序执行(如示例7-7所示)
Code Example7-7多线程合作(Barrier同步)
While(a_great_many_iterations){
Sequential_computation
Parallel_computation
}
例如,你也许通过严格的线形计算获得了一个矩阵,然后对这个矩阵进行并
行计算,再用运行结果创建另外一个矩阵,然后再进行并行计算,等等等等。
此类计算的并行算法的特点是在计算时不需要太多同步,但使用结果时必须保
证结果都已得出。
如果执行并行计算的时间远比创建和同步线程的时间长,那么线程的创建和
同步不成问题。但是如果运算时间不是很长,线程的创建和同步时间就显得非常重
要。
7.2小结
这个编程指南包含了线程编程的基本注意事项。参看附录A"示例应用程序",可
以看到很多讨论过的特点和风格。
推荐读物:
Algorithms for Mutual Exclusion by Michel Raynal (MIT Press, 1986)
Concurrent Programming by Alan Burns & Geoff Davies(Addison-Wesley, 1993)
Distributed Algorithms and Protocols by Michel Raynal (Wiley, 1988)
Operating System Concepts by Silberschatz, Peterson, & Galvin
(Addison-Wesley, 1991)
Principles of Concurrent Programming by M. Ben-Ari (Prentice-Hall, 1982)
Mccartney
更多阅读
如何查看linux系统版本 查看linux系统的位数 linux系统查看jdk版本
如何查看linux系统版本 查看linux系统的位数——简介 之前咗嚛介绍了如何查看linux系统下,各文件系统版本。根本经验主要看看linux下系统版本和操作系统位数如何查看。对于windows而已,linux版本有不同的版本号和内核等,本经验以Centos
ssh命令linux系统远程连接linux系统的方法 ssh远程登录命令
ssh命令linux系统远程连接linux系统的方法用linux系统的ssh命令远程连接另一台linux机器的命令#ssh 用户名@主机名(IP地址)例如:#ssh root@192.168.1.25意思是linux系统下用命令连接另一台机器是用root帐户连接 192.168.1.25 机器
原创 评《藏婚藏地奇异一妻多夫婚姻下的情感与生活 》 情感的小屋 原创
评《藏婚(藏地奇异一妻多夫婚姻下的情感与生活)》右岸左人小说《藏婚》作者多吉卓嘎,西藏女作家,汉名张羽芊,网名沙草。关于她的经历,《西藏商报》曾介绍说“作者张羽芊在西藏定居已有十年,在这十年的游走中渐渐熟悉西藏,能说一口流利的
怎样修改WIN7下的host文件 win7没有权限修改host
Win7下因为权限问题会导致不能更改hosts文件,这让人很是苦恼.下面的几种方法很有效.希望对大家有用.怎样修改WIN7下的host文件——方法一第一种方法是网上流传很广的覆盖方法.就是先复制hosts文件到别的地方,修改完了再覆盖回来就搞
libcurl在vc6下的安装这个狂赞的,按照以下步骤使用libcurl绝对 libcurl.dll
libcurl在vc6下的安装(超赞)(http://blog.chinaunix.net/u/25096/showart_388890.html)libcurl是一个很好的库,免费开源的,客户端url传输库,支持FTP,FTPS,TFTP,HTTP,HTTPS,GOPHER,TELNET,DICT,FILE和LDAP,跨平台,支持Windows,Unix,Linux等,线程安全,支持