精华区 [关闭][返回]

当前位置:网易精华区>>讨论区精华>>编程开发>>C/C++>>技术精解:内存、进程、线程等>>转贴多线程编程指南

主题:转贴多线程编程指南
发信人: skyice()
整理人: wenbobo(2002-12-06 22:56:22), 站内信件
今天从同事那里看到从清华download下的文章,一并贴出来给大家欣赏

发信人: Mccartney (coolcat), 信区: Unix
标  题: Solaris2.4 多线程编程指南1--线程基础
发信站: BBS 水木清华站 (Sun May 17 16:24:23 1998)

1线程基础
    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(可以跟踪一个程序的系统调用和信
号),可以很好地报告线程的状态。

发信人: Mccartney (coolcat), 信区: Unix
标  题: Solaris2.4 多线程编程指南2--用多线程编程
发信站: BBS 水木清华站 (Sun May 17 16:25:45 1998)

2 用多线程编程

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 <thread.h>
    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.h>
        thread_t thr_self(void)
        返回值--调用者的线程号。

2.1.3放弃执行

    thr_yield(3T)
    thr_yield停止执行当前线程,将执行权限让给有相同或更高优先权的线
程。
        #include <thread.h>
        void thr_yield(void);

2.1.4挂起或继续执行线程

thr_suspend(3T) 挂起线程。
        #include <thread.h>
        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 <thread.h>
        int thr_continue(thread_t target_thread); 
    一个挂起的线程不会被信号唤醒。信号被挂起知道线程被thr-continue
恢复执行。
   返回值--成功执行后返回0。其他值意味着错误。在以下情况发生时,函数
失败并返回相关值。
        ESRCH:target_thread在当前进程中找不到。

2.1.5向线程发信号

thr_kill(3T)向线程发信号
        #include <thread.h>
        #include <signal.h>
        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 <thread.h>
        #include <signal.h>
        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 <thread.h>
        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 <thread.h>
    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 <thread.h>
        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 <thread.h>
        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 <thread.h>
        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,"%s\n",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 <thread.h>
        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 <thread.h>
        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 <thread.h>
        int thr_getconcurrency(void)
    返回值--thr_getconcurrency()为期望的同时性等级返回当前值。

2.1.13.2 Thr_setconcurrency(3T)

    用thr_setconcurrency()设置期望的同时性等级。
        #include <thread.h>
        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 <thread.h>
        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 <thread.h>
        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和调度等级"。

发信人: Mccartney (coolcat), 信区: Unix
标  题: Solaris2.4 多线程编程指南4--操作系统编程
发信站: BBS 水木清华站 (Sun May 17 16:31:05 1998)

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=%d\n",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=%d\n",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=%d\n",a,b,c,d);
                     }
              }
        }
        void problem(int sig)
        {
              printf("couldn't deal with them,try again\n");
              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 <sys/asynch.h>
     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 <sys/types.h>
        #include <unistd.h>
        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()放在循环内部。

发信人: Mccartney (coolcat), 信区: Unix
标  题: Solaris2.4 多线程编程指南5--安全和不安全的接口函?
发信站: BBS 水木清华站 (Sun May 17 16:32:40 1998)

5. 安全和不安全的接口

本章定义了函数和库的多线程安全等级。
        线程安全
        多线程接口安全等级
        异步安全函数
        库的多线程安全等级

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的,则只有在一个线程的调用时才是安全的。



发信人: Mccartney (coolcat), 信区: Unix
标  题: Solaris2.4 多线程编程指南6--编译和调试
发信站: BBS 水木清华站 (Sun May 17 16:33:41 1998)

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"标志。
    这个标志必须在编译应用程序的每一个模块时都使用

[关闭][返回]