1.
3 使用同步对象来编程
本章定义了四种可用的同步类型,并且讨论实现同步的注意事项。
互斥锁(mutex)
条件变量(condition variable)
多读单写锁(multi-read,single-write lock)
信号量(semophore)
进程间同步(process synchronization)
同步原语的比较(compare primitive)
同步对象是内存中的变量,你可以象访问一般的数据那样来访问它。不同进程内的线程可以通过共享内存中的同步变量来同步,即使这些线程互不可见。
同步变量可以放置在文件当中,可以比创建它的进程拥有更长的生命。
同步对象的类型包括:
· 互斥锁
· 状态变量
· 读写锁
· 信号灯(信号量)
在下面几种情况下,同步是重要的:
· 在两个或更多个进程内的线程可以合用一个同步变量。注意,同步变量应当被一个进程初始化,在第二次初始化时,该同步变量被设置为解锁状态。
· 同步是唯一保证共享数据持久的办法。
· 一个进程可以映射一个文件并通过一个线程将其加锁,修改完成之后,该线程释放文件锁并恢复文件。在文件加锁的过程中,任何程序中的任何 线程想要加锁时都会阻塞,直至解锁;
· 同步可以保证易变数据的安全。
· 同步对于简单变量也是很重要的,例如整数。在整数没有和总线对齐或
大于数据宽度的情况下,读写一个整数可能需要多个内存周期。虽然在SPARC系统上不会发生这样的情况,但移植程序时不能不考虑这一点;
3.1互斥锁
用互斥锁可以使线程顺序执行。互斥锁通常只允许一个线程执行一个关键部分的代码,来同步线程。互斥锁也可以用来保护单线程代码。
Table 3-1 互斥锁函数
函数 操作
Mutex_init(3T) 初始化一个互斥锁
Mutext_lock(3T) 给一个互斥锁加锁
Mutex_trylock(3T) 加锁,如失败不阻塞
Mutex_unlock(3T) 解锁
Mutex_destroy(3T) 解除互斥状态
如果两个进程有共享且可写的内存,且做了相应的初始化设置后(参见mmap(2)),互斥锁可以实现进程间的线程同步。
互斥锁在使用前一定要初始化。
多线程等待一个互斥锁时,其获得互斥锁的顺序是不确定的。
3.1.1初始化一个互斥锁
mutex_init(3T)
#include ( or #include )
int mutex_init(mutex_t *mp, int type, void * arg);
用mutex_init()来初始化一个由mp指向的互斥锁。Type可以是以下值之一(arg现在先不谈)。
USYNC_PROCESS 互斥锁用来同步进程间的线程。
USYNC_THREAD 互斥锁只用来同步进程内部的线程。
互斥锁也可以通过分配零内存来初始化,在此种情况下应当设定USYNC_THREAD。
一定不会有多个线程同时初始化同一个互斥锁。一个互斥锁在使用期间一定不会被重新初始化。
返回值--mutex_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp或者arg指向一个非法地址。
3.1.2给互斥锁加锁
mutex_lock(3T)
#include (or #include )
int mutex_lock(mutex_t *mp);
用mutex_lock()锁住mp指向的互斥锁。如果mutex已经被锁,当前调用线程阻塞直到互斥锁被其他线程释放(阻塞线程按照线程优先级等待)。当mutex_lock()返回,说明互斥锁已经被当前线程成功加锁。
返回值--mutex_lock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.1.3加非阻塞互斥锁
mutex_trylock(3T)
#include (or #include )
int mutex_trylock(mutex_t *mp);
用mutex_trylock()来尝试给mp指向的互斥锁加锁。这个函数是mutex_lock()的非阻塞版本。当一个互斥锁已经被锁,本调用返回错误。否则,互斥锁被调用者加锁。
返回值--mutex_trylock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
EBUSY mp指向的互斥锁已经被锁。
3.1.4给互斥锁解锁
mutex_unlock(3T)
#include (or #include )
int mutex_unlock(mutex_t *mp);
用mutex_unlock()给由mp指向的互斥锁解锁。互斥锁必须处于加锁状态且调用本函数的线程必须是给互斥锁加锁的线程。如果有其他线程在等待互斥锁,在等待队列头上的线程获得互斥锁并脱离阻塞状态。
返回值--mutex_unlock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.1.5清除互斥锁
mutex_destroy(3T)
#include (or #include )
int mutex_destroy(mutex_t *mp);
用mutex_destroy()函数解除由mp指向的互斥锁的任何状态。储存互斥锁的内存不被释放。
返回值--mutex_destroy()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.1.6互斥锁代码示例
Code Example 3-1 Mutex Lock Example
Mutex_t count_mutex;
Int count;
Increment_count()
{ mutex_lock(&count_mutex);
count=count+1;
mutex_unlock(&cout_mutex);
}
int get_count()
{ int c;
mutex_lock(&count_mutex);
c=count;
mutex_unlock(&count_mutex);
return(c);
}
在示例3-1中两个函数用互斥锁实现不同的功能,increment_count()保证对共享变量的一个原子操作(即该操作不可中断),get_count()用互斥锁保证读取count期间其值不变。
*为锁设置等级
你可能会需要同时访问两种资源。也许你在用其中一种资源时,发现需要另外一 种。就象我们在示例3-2中看到的,如果两个线程希望占有两种资源,但加互斥锁的 顺序不同,有可能会发生问题。在这个例子当中,两个线程分别给互斥锁1和2加锁, 在它们想给另外的资源加锁的时候,将会发生死锁。
Code Example 3-2 Deadlock
Thread 1:
Mutex_lock(&m1)
/* use resource 1*/
mutex_lock(&m2);
/* use resources 1 and 2*/
mutex_unlock(&m2);
mutex_unlock(&m1);
Thread 2:
Mutex_lock(&m2);
/*use resource 2*/
mutex_lock(&m1);
/* use resources 1 and 2*/
mutex_unlock(&m1);
mutex_unlock(&m2);
避免这个问题的最好办法是在线程给多个互斥锁加锁时,遵循相同的顺序。这种技术的一种实现叫"锁的等级":在逻辑上为每个锁分配一个数进行排序。
如果你已经拥有一个等级为I的互斥锁,你将不能给等级小于I的互斥锁加锁。
---------------------------------------
注意--lock_init可以检测这个例子当中死锁的类型。避免死锁的最好办法是采用等
级锁:如果对互斥锁的操作遵循一个预先定义的顺序,死锁将不会发生。
---------------------------------------
但是,这种技术并非总可以使用--有时你必须对互斥锁进行不按照预定义顺序的 操作。为了在这种情况下阻止死锁,一个线程在发现死锁用其他方法无法避免时, 必须释放已经占有的所有资源。示例3-3显示了这种方法。
Code Example 3-3 条件锁
Thread 1:
Mutex_lock(&m1);
Mutex_lock(&m2);
Mutex_unlock(&m2);
Mutex_unlock(&m1);
Thread 2:
For(;{
Mutex_lock(&m2);
If(mutex_trylock(&m1)==0)
/*got it*/
break;
/*didn't get it */
mutex_unlock(&m1);
}
mutex_unlock(&m1);
mutex_unlock(&m2);
在上例中,线程1按照预定的顺序加锁,但线程2打乱了次序。为避免死锁,线程2必须小心操作互斥锁1:如果设置在等待互斥锁释放时阻塞,则可能导致死锁。
为保证上述情况不会发生,线程2调用mutex_trylock,如果互斥锁可用则用, 不可用则立刻返回失败。在这个例子当中,线程2一定要释放互斥锁2,以便线程1 可以使用互斥锁1和互斥锁2。
3.1.7锁内嵌于单链表当中
示例3-4同时占有3个锁,通过锁等级定义避免死锁。
Code Example 3-4 单链表结构
Typedef struct node1{
Int value;
Struct node1 *link;
Mutex_t lock;
}node1_t;
node1_t Listhead;
此例利用单链表结构的每一个节点存储一个互斥锁。为了删除一个互斥锁,要从listhead开始搜索(它本身不会被删除),知道找到指定的节点。
为了保证同时删除不会发生,在访问其内容之前要先锁定节点。因为所有的搜索从listhead开始按顺序进行,所以不会出现死锁。
如果找到指定节点,对该节点和其前序节点加锁,因为两个节点都需要改变。因为前序节点总是首先加锁,死锁将不会发生。
下面C程序从单链表中删除一项。
Code Example 3-5 内嵌锁的单链表
Node1_t * delete(int value){
Node1_t * prev, *current;
Prev =&listhead;
Mutex_lock(&prev->lock);
While((current=prev->link)!=NULL){
Mutex_lock(¤t->lock);
If(current->value==value){
Prev->link=current->link;
Mutex_unlock(¤t->lock);
Mutex_unlock(&prev->lock);
Current->link=NULL;
Return(current);
}
mutex_unlock(&prev->lock);
prev=current;
}
mutex_unlock(&prev->lock);
return(NULL);
}
3.1.8内嵌在环状链表中的锁
示例3-6把前例的单链表改为环链表。环链表没有显式的表头;一个线程可以和某个节点连接,对该节点及其邻节点进行操作。等级锁在这里不容易使用,因为其链表是环状的。
Code Example 3-6 Circular Linked List Structure
Typedef struct node 2 {
Int value;
Struct node2 *link;
Mutex_t lock;
} node2_t;
下面的C程序给两个节点加锁,并对它们做操作。
Code Example 3-7 内嵌锁的环链表
Void Hit Neighbor(node2_t *me){
While(1){
Mutex_lock(&me->lock);
If(mutex_lock(&me->link->lock)){
/* failed to get lock*/
mutex_unlock(&me->lock);
continue;
}
break;
}
me->link->value += me->value;
me->value /=2;
mutex_unlock(&me->link->lock);
mutex_unlock(&me->lock);
}
3.2条件变量
用条件变量来自动阻塞一个线程,直到某特殊情况发生。通常条件变量和互斥锁同时使用。
Table3-2 有关条件变量的函数
函数 操作
Cond_init(3T) 初始化条件变量
Cond_wait(3T) 基于条件变量阻塞
Cond_signal(3T) 解除指定线程的阻塞
Cond_timedwait(3T) 阻塞直到指定事件发生
Cond_broadcast(3T) 解除所有线程的阻塞
Cond_destroy(3T) 破坏条件变量
通过条件变量,一个线程可以自动阻塞,直到一个特定条件发生。条件的检测是在互斥锁的保护下进行的。
如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如 果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它 的线程,重新获得互斥锁,重新评价条件。
如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
使用条件变量之前要先进行初始化。而且,在有多个线程等待条件变量时,它们解除阻塞不存在确定的顺序。
3.2.1初始化条件变量
cond_init(3T)
#include (or #include )
int cond_init(cond_t *cvp, int type, int arg);
用cond_init()初始化有cvp指向的条件变量。Type可以是如下值之一(arg先
不谈):
USYNC_PROCESS 条件变量可以在进程间实现线程同步;
USYNC_THREAD 条件变量只能在进程内部对线程同步;
条件变量可以用分配零内存来初始化,在这种情况下一定要是USYNC_THREAD。
多线程不能同时初始化同一个条件变量。如果一个条件变量正在使用,它不能被重新初始化。
返回值--cond_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.2.2关于条件变量阻塞
cond_wait(3T)
#include (or #include )
int cond_wait(cond_t *cvp, mutex_t *mp);
用cond_wait()释放由mp 指向的互斥锁,并且使调用线程关于cvp指向的条件 变量阻塞。被阻塞的线程可以被cond_signal(), cond_broadcast(),或者由fork() 和传递信号引起的中断唤醒。
与条件变量关联的条件值的改变不能从cond_wait()的返回值得出,这样的状 态必须被重新估价。
即使是返回错误信息,Cond_wait()通常在互斥锁被调用线程加锁后返回。
函数阻塞直到条件被信号唤醒。它在阻塞前自动释放互斥锁,在返回前在自动 获得它。
在一个典型的应用当中,一个条件表达式在互斥锁的保护下求值。如果条件表 达式为假,线程基于条件变量阻塞。当一个线程改变条件变量的值时,条件变量获 得一个信号。这使得等待该条件变量的一个或多个线程退出阻塞状态,并试图得到 互斥锁。
因为在被唤醒的线程的cond_wait()函数返回之前条件已经改变,导致等待的 条件在得到互斥锁之前必须重新测试。推荐的办法是在while循环中写条件检查。
Mutex_lock();
While(condition_is_false)
Cond_wait();
Mutes_unlock();
如果有多个线程关于条件变量阻塞,其退出阻塞状态的顺序不确定。
返回值--cond_wait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
EINTR 等待被信号或fork()中断。
3.2.3使指定线程退出阻塞状态
cond_signal(3T)
#include (or #include )
int cond_signal (cond_t *cvp);
用cond_signal()使得关于由cvp指向的条件变量阻塞的线程退出阻塞状态。在 同一个互斥锁的保护下使用cond_signal()。否则,条件变量可以在对关联条件变量 的测试和cond_wait()带来的阻塞之间获得信号,这将导致无限期的等待。
如果没有一个线程关于条件变量阻塞,cond_signal无效。
返回值--cond_signal()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
Code Example 3-8 使用cond_wait(3T)和cond_signal(3T)的例子
Mutex_t count_lock;
Cond_t count_nonzero;
Unsigned int count;
Decrement_count()
{
mutex_lock(&count_lock);
while(count==0)
cond_wait(&count_nonzero,&count_lock);
count=count-1;
mutex_unlock(&count_lock);
}
increment_count()
{
mutex_lock(&count_lock);
if(count==0)
cond_signal(&count_nonzero);
count=count+1;
mutex_unlock(&count_lock);
}
3.2.4阻塞直到指定事件发生
cond_timedwait(3T)
#include (or #include )
int cond_timedwait(cond_t *cvp, mutex_t *mp,
timestruc_t *abstime);
cond_timedwait()和cond_wait()用法相似,差别在于cond_timedwait()在经过有abstime指定的时间时不阻塞。
即使是返回错误,cond_timedwait()也只在给互斥锁加锁后返回。
Cond_timedwait()函数阻塞,直到条件变量获得信号或者经过由abstime指定 的时间。Time-out被指定为一天中的某个时间,这样条件可以在不重新计算 time-out值的情况下被有效地重新测试,???就象在示例3-9中那样。
返回值--cond_timedwait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 由abstime 指定的时间大于应用程序启动的时间加50,000,000,或者纳秒数大于等于1,000,000,000。
EFAULT cvp指向一个非法地址。
EINTR 等待被信号或fork()中断。
ETIME abstime指定的时间已过。
Code Example 3-9 时间条件等待
Timestruc_t to;
Mutex_t m;
Cond_t c;
Mutex_lock(&m);
To.tv_sec=time(NULL)+TIMEOUT;
To.tv_nsec=0;
While (cond==FALSE){
Err=cond_timedwait(&c,&m,&to);
If(err=ETIME) {
/* TIMEOUT, do something */
break;
}
}
mutex_unlock(&m);
3.2.5使所有线程退出阻塞状态
cond_broadcast(3T)
#include ( or #include )
int cond_wait(cond_t *cvp);
用cond_broadcast()使得所有关于由cvp指向的条件变量阻塞的线程退出阻塞状态。如果没有阻塞的线程,cond_broadcast()无效。
这个函数唤醒所有由cond_wait()阻塞的线程。因为所有关于条件变量阻塞的线程都同时参与竞争,所以使用这个函数需要小心。
例如,用cond_broadcast()使得线程竞争变量资源,如示例3-10所示。
Code Example 3-10 条件变量广播
Mutex_t rsrc_lock;
Cond_t rsrc_add;
Unsigned int resources;
Get_resources(int amount)
{ mutex_lock(&rsrc_lock);
while(resources < amount) {
cond_wait(&rsrc_add, &rsrc_lock);
}
resources-=amount;
mutex_unlock(&rsrc_lock);
}
add_resources(int amount)
{
mutex_lock(&rsrc_lock);
resources +=amount;
cond_broadcast(&rsrc_add);
mutex_unlock(&rsrc_lock);
}
注意,在互斥锁的保护内部,首先调用cond_broadcast()或者首先给resource增值,效果是一样的。
返回值--cond_broadcast()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
在互斥锁的保护下调用cond_broadcast()。否则,条件变量可能在检验关联状态和通过cond_wait()之间获得信号,这将导致永久等待。
3.2.6清除条件变量
cond_destroy(3T)
#include ( or #include )
int cond_destroy(cond_t *cvp);
使用cond_destroy() 破坏由cvp指向的条件变量的任何状态。但是储存条件变量的空间将不被释放。
返回值--cond_destroy()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
3.2.7唤醒丢失问题
在没有互斥锁保护的情况下调用cond_signal()或者cond_broadcast()会导致丢 失唤醒问题。一个唤醒丢失发生在信号或广播已经发出,但是线程即使在条件为真 时仍然关于条件变量阻塞,具体地说,这发生在调用cond_signal()时并没有获得互 斥锁的情况下。
如果一个线程已经作过条件检验,但是尚未调用cond_wait(),这时另外一个线 程调用cond_signal(),因为没有已被阻塞的线程,唤醒信号丢失。
3.2.8生产者/消费者问题
这个问题是一个标准的、著名的同时性编程问题的集合:一个有限缓冲区和两类线程,生产者和消费者,他们分别把产品放入缓冲区和从缓冲区中拿走产品。
一个生产者在缓冲区满时必须等待,消费者在缓冲区空时必须等待。
一个条件变量代表了一个等待条件的线程队列。
示例3-11有两个队列,一个(less)给生产者,它们等待空的位置以便放入信 息;另外一个(more)给消费者,它们等待信息放入缓冲区。这个例子也有一个互 斥锁,它是一个结构,保证同时只有一个线程可以访问缓冲区。
下面是缓冲区数据结构的代码。
Code Example 3-11 生产者/消费者问题和条件变量
Typedef struct{
Char buf[BSIZE];
Int occupled;
Int nextin;
Int nextout;
Mutex_t mutex;
Cond_t more;
Cond_t less;
}buffer_t;
buffer_t buffer;
如示例3-12所示,生产者用一个互斥锁保护缓冲区数据结构然后确定有足够的空 间来存放信息。如果没有,它调用cond_wait(),加入关于条件变量less阻塞的线程 队列,说明缓冲区已满。这个队列需要被信号唤醒。
同时,作为cond_wait()的一部分,线程释放互斥锁。等待的生产者线程依赖于 消费者线程来唤醒。当条件变量获得信号,等待less的线程队列里的第一个线程被唤 醒。但是,在线程从cond_wait()返回前,必须获得互斥锁。
这再次保证了线程获得对缓冲区的唯一访问权。线程一定要检测缓冲区有足够的 空间,如果有的话,它把信息放入下一个可用的位置里。
同时,消费者线程也许正在等待有信息放入缓冲区。这些线程等待条件变量more。 一个生产者线程,在刚刚把信息放入存储区后,调用cond_signal()来唤醒下一个等 待的消费者。(如果没有等待的消费者,这个调用无效。)最后,生产者线程释放互 斥锁,允许其他线程操作缓冲区。
Code Example 3-12 生产者/消费者问题--生产者
Void producer(buffer_t *b, char item) {
Mutex_lock(&b->mutex);
While ( b->occupied >= BSIZE)
Cond_wait(&b->less, &b->mutex);
Assert(b->occupied < BSIZE);
b->buf(b->nextin++)=item;
b->nextin %=BSIZE;
b->occupied ++;
/* now: either b->occupied < BSIZE and b->nextin is the index
of the next empty slot in the buffer, or
b->occupied == BSIZE and b->nextin is the index of the
next (occupied) slot that will be emptied by a consumer
(such as b-> == b->nextout) */
cond_signal(&b->more);
mutex_unlock(&b->mutex);
}
注意assert()命令的用法;除非代码用NDEBUG方式编译,assert()在参数为真时 (非零值)不做任何操作,如果参数为假(参数为假),程序退出。
这种声明在多线程编程中特别有用--在失败时它们会立刻指出运行时的问题, 它们还有其他有用的特性。
后面说明代码可以更加称得上是声明,但它太过复杂,无法用布尔表达式来表达,所以用文字来写。???
声明和说明???都是不变量的实例。它们都是一些逻辑命题,在程序正常执行时不应当被证伪,除非一个线程试图改变非变量说明段的变量。???
不变量是一种极为有用的技术。即使它们没有在程序中写出,在分析程序中也需要把它们看成不变量。
生产者代码中的不变量(说明部分)在程序执行到这一段时一定为真。如果你把这段说明移到mutex_unlock()后面,它将不一定保持为真。如果将其移到紧跟着声明的后面,它仍然为真。
关键在于,不变量表现了一个始终为真的属性,除非一个生产者或一个消费者正 在改变缓冲区的状态。如果一个线程正在操作缓冲区(在互斥锁的保护下),它将暂 时将不变量置为假。但是,一旦线程结束对缓冲区的操作,不变量会立刻恢复为真。
示例3-13为消费者的代码。它的流程和生产者是对称的。
Code Example 3-13 生产者/消费者问题--消费者
Char consumer(buffer_t *b){
Char item;
Mutex_lock(&b->mutex);
While(b->occupied <=0)
Cond_wait(&b->more, &b->mutex);
Assert(b->occupied>0);
Item=b->buf(b->nextout++);
b->nextout %=BSIZE;
b->occupied--;
/* now: either b->occupied>0 and b->nextout is the index of
the nexto ccupied slot in the buffer, or b->occupied==0
and b->nextout is the index of the next(empty) slot that
will be filled by a producer (such as b->nextout ==b->nextin) */
cond_signal(&b->less);
mutex_unlock(&b->mutex);
return(item);
}
3.3多读单写锁
读写锁允许多个线程同时进行读操作,但一个时间至多只有一个线程进行写操作。
表3-3 读写锁的函数
函数 操作
rwlock_init(3T) 初始化一个读写锁
rw_rdlock(3T) 获得一个读锁
rw_tryrdlock(3T) 试图获得一个读锁
rw_wrlock(3T) 获得一个写锁
rw_trywrlock(3T) 试图获得一个写锁
rw_unlock(3T) 使一个读写锁退出阻塞
rwlock_destroy(3T) 清除读写锁状态
如果任何线程拥有一个读锁,其他线程也可以拥有读锁,但必须等待写锁。如 果一个线程拥有写锁,或者正在等待获得写锁,其它线程必须等待获得读锁或写锁。
读写锁比互斥锁要慢,但是在所保护的数据被频繁地读但并不频繁写的时候可以提高效率。
如果两个进程有共享的可读写的内存,可以在初始化时设置成用读写锁进行进程间的线程同步。
读写锁使用前一定要初始化。
3.3.1初始化一个读写锁
rwlock_init(3T)
#include (or #include )
int rwlock_init(rwlock_t *rwlp, int type, void * arg);
用rwlock_init()来初始化由rwlp指向的读写锁并且设置锁的状态为没有锁。
Type可以是如下值之一(arg现在先不谈)。
USYNC_PROCESS 读写锁可以实现进程间的线程同步。
USYNC_THREAD 读写锁只能在进程内部实现线程同步。
多线程不能同时初始化一个读写锁。读写锁可以通过分配零内存来初始化,在这种情况下,一定要设置USYNC_THREAD。一个读写锁在使用当中不能被其他线程重新初始化。
返回值--rwlock_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp或arg指向一个非法地址。
3.3.2获得一个读锁
rw_rdlock(3T)
#include (or #include )
int rw_rdlock(rwlock_t *rwlp);
用rw_rdlock()来给一个由rwlp指向的读写锁加上读锁。如果读写锁已经被加写锁,则调用线程阻塞直到写锁被释放。否则,读锁将被成功获得。
返回值--rw_rdlock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
3.3.3试图获得一个读锁
rw_tryrdlock(3T)
#include (or #include )
int rw_tryrdlock(rwlock_t *rwlp);
试图给读写锁加读锁,如果读写锁已经被加写锁,则返回错误,而不再进入阻塞状态。否则,读锁将被成功获得。
返回值--rw_tryrdlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
EBUSY 由rwlp指向的读写锁已经被加写锁。
3.3.4获得一个写锁
rw_wrlock(3T)
#include (or #include )
int rw_wrlock(rwlock_t *rwlp);
用rw_wrlock()为由rwlp指向的读写锁加写锁。如果该读写锁已经被加读锁或写锁,则调用线程阻塞,直到所有锁被释放。一个时刻只有一个线程可以获得写锁。
返回值--rw_wrlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
3.3.5试图获得写锁
rw_trywrlock(3T)
#include (or #include )
int rw_trywrlock(rwlock_t *rwlp);
用rw_trywrlock()试图获得写锁,如果该读写锁已经被加读锁或写锁,它将返回错误。
返回值--rw_trywrlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
EBUSY 由rwlp指向的读写锁已被加锁。
3.3.6使一个读写锁退出阻塞状态
rw_unlock(3T)
#include (or #include )
int rwlock_tryrdlock(rwlock_t *rwlp);
用rw_unlock()来使由rwlp指向的读写锁退出阻塞状态。调用线程必须已经获得对该读写锁的读锁或写锁。如果任何其它线程在等待读写锁可用,它们当中的一个将退出阻塞状态。
返回值--rw_unlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
3.3.7清除读写锁
rwlock_destroy(3T)
#include (or #include )
int rwlock_destroy(rwlock_t *rwlp);
使用rwlock_destroy()来取消由rwlp指向的读写锁的状态。存储读写锁的空间不被释放。
返回值--rw_destroy ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
示例3-14用一个银行帐户来演示读写锁。如果一个程序允许多个线程同时进行读操作,一个时刻只有一个写操作被允许。注意get_balance()函数通过锁来保证检查和储存操作是原子操作。
Code Example 3-14 读/写银行帐户
Rwlock_t account_lock;
Float checking_balance=100.0;
Float saving_balance=100.0;
… …
rwlock_init (&account_lock, 0, NULL);
… …
float get_balance(){
float bal;
rw_rdlock(&account_lock);
bal=checking_balance +saving_balance;
rw_unlock(&account_lock);
return(bal);
}
void tranfer_checking_to_savings(float amount) {
rw_wrlock(&account_lock);
checking_balance=checking_balance - amount;
savings_balance=savings_balance +amount;
rw_unlock(&account_lock);
}
3.4信号量(信号灯)
信号灯是E.W.Dijkstra在60年代晚期定义的程序结构。Dijkstra的模型是一个铁路上的操作:一段单线铁路在一个时刻只允许一列火车通过。
用一个信号灯来维护这段铁路。一列火车在进入单线铁路之前必须等待信号灯 的许可。如果一列火车进入这段轨道,信号灯改变状态,以防止其他火车进入。在 火车离开这段轨道时,必须将信号灯复原,使得其他火车得以进入。
在信号灯的计算机版本中,一个信号灯一般是一个整数,称之为信号量。一个 线程在被允许进行后对信号量做一个p操作。
P操作的字面意思是线程必须等到信号量的值为正(positive)才能继续进行, 进行前先给信号量减1。当做完相关的操作时(相当于离开铁轨),线程执行一个 v操作,即给信号量加1。这两个操作必须具有不可中断性,也叫不可分性,英文字 面为原子性(atomic),即他们不能被分成两个子操作,在子操作之间还可以插入 其它线程的其他操作,这些操作可能改变信号量。在P操作中,信号量的值在被减之 前一定要为正(使得信号量在被减1之后不会为负)。
在P操作或V操作当中,操作不会互相干扰。如果两个V操作要同时执行,则信号量的新值比原来大2。
记住P和V本身是什么意思已经不重要了,就象记住Dijkstra是荷兰人一样。但 是,如果引起了学者考证的兴趣,P代表prolagen,一个由proberen de verlagen演 变来的合成词,它的意思是"试图减"。V代表verhogen,它的意思是"增加"。这些在 Dijkstra的技术笔记EWD 74中提到过。
Sema_wait(3T)和sema_post(3T)分别对应Dijkstra的P和V操作, sema_trywait(3T)是P操作的一个可选的形式,在P操作不能执行时,线程不会阻塞, 而是立刻返回一个非零值。
有两种基本的信号量:二值信号量,其值只能是0或者1,和计数信号量,可以 是非负值。一个二值信号量在逻辑上相当于一个互斥锁。
然而,尽管并不强制,互斥锁应当被认为只能被拥有锁的线程释放,而"拥有信 号量的线程"这个概念是不存在的,任何线程都可以进行一个V操作 (或sema_post(3T))。
计数信号量的功能大概和与互斥锁合用的条件变量一样强大。在很多情况下, 采用信号量的程序比采用条件变量要简单一些(如下面的例子所示)。
然而,如果一个互斥锁和条件变量一起使用,有一个隐含的框架,程序的哪一 部分被保护是明显的。在信号量则不然,它可以用同时性编程当中的go to 来调用, 它更适合用于那些结构性不强的,不精确的方面。
3.4.1计数信号量
在概念上,一个信号量是一个非负整数。信号量在典型情况下用来协调资源, 信号量一般被初始化为可用资源的数量。线程在假如资源是给计数器加1,在拿走资 源时给计数器减1,操作都具有原子性。
如果一个信号量的值变为0,表明已无可用资源,想要给信号量减1的操作必须 等到它为正时。
表3-4 信号量函数
函数 操作
Sema_init(3T) 初始化信号量
Sema_post(3T) 增加信号量
Sema_wait(3T) 关于信号量阻塞
Sema_trywait(3T) 减少信号量
Sema_destroy(3T) 破坏信号量的状态
因为信号量不被哪个线程占有,它们可以用异步事件来通知(例如信号处理器)。 而且,因为信号量包含状态,他们可以被异步使用???,而不用象条件变量那样 一定要先获得互斥锁。
缺省情况下,等待信号量的多个线程退出阻塞的顺序是不确定的。
信号量在使用前一定要初始化。
3.4.2初始化一个信号量
sema_init(3T)
#include (or #include )
int sema_init(sema_t *sp, unsigned int count, int type, void *arg);
sema_init用count的值来初始化由sp指向的信号量。Type可以是如下值之一(arg先不谈)。
USYNC_PROCESS 信号量可以在进程间进行线程同步。只有一个进程需要初始化
信号量。Arg忽略。
USYNC_THREAD 信号量只能在进程内部进行线程同步。
多个线程不能同时初始化同一个信号量。一个信号量在使用中不能被其他线程重新初始化。
返回值--sema_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp或arg指向一个非法地址。
3.4.3给信号量增值
sema_post(3T)
#include (or #include )
int sema_destroy(sema_t *sp);
用sema_post()给由sp指向的信号量原子地(表示其不可分性,下同)增1,如果有其它线程关于信号量阻塞,其中一个退出阻塞状态。
返回值--sema_post()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
3.4.4关于一个信号量阻塞
sema_wait(3T)
#include (or #include )
int sema_wait(sema_t *sp)
用sema_wait()使得调用线程在由sp指向的信号量小于等于零时阻塞,在其大于零原子地对其进行减操作。
返回值--sema_wait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
EINTR 等待被信号或fork()打断。
3.4.5给信号量减值
sema_trywait(3T)
#include (or #include )
int sema_trywait(sema_t *sp)
用sema_trywait()在sp比零大时对它进行原子地减操作。是sema_wait()的非阻塞版本。
返回值--sema_trywait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
EBUSY sp 指向的值为零。
3.4.6清除信号量的状态
sema_destroy(3T)
#include (or #include )
int sema_destroy(sema_t *sp)
用sema_destroy(3T)破坏与sp指向的信号量关联的任何状态,但空间不被释放。
返回值--sema_destroy()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
3.4.7用信号量解决生产者/消费者问题
示例3-15所示的程序与条件变量的解决方案类似;两个信号量代表空和满的缓冲区的数目,生产者线程在没有空缓冲区时阻塞,消费者在缓冲区全空时阻塞。
Code Example 3-15 用信号量解决的生产者/消费者问题
Typedef struct{
Char buf[BSIZE];
Sema_t occupied;
Sema_t empty;
Int nextin;
Int nextout;
Sema_t pmut;
Sema_t cmut;
} buffer_t;
buffer_t buffer;
sema_init(&buffer.occupied, 0, USYNC_THREAD, 0);
sema_init(&buffer.empty, BSIZE, USYNC_THREAD, 0);
sema_init(&buffer.pmut, 1, USYNC_THREAD, 0);
sema_init(&buffer.cmut, 1, USYNC_THREAD, 0);
buffer.nextin=buffer.nextout =0;
另外一对信号量与互斥锁作用相同,用来在有多生产者和多个空缓冲区的情况下,或者是有多个消费者和多个满的缓冲区的情况下控制对缓冲区的访问。互斥锁同样可以工作,但这里主要是演示信号量的例子。
Code Example 3-16 生产者/消费者问题--生产者
Void producer(buffer_t *b, char item){
Sema_wait(&b->empty);
Sema_wait(&b->pmut);
b->buf[b->nextin]=item;
b->nextin++;
b->nextin %=BSIZE;
sema_post( &b->pmut);
sema_post(&b->occupied);
}
Code Example 3-17 生产者/消费者问题--消费者
Char consumer(buffer_t *b){
Char item;
Sema_wait(&b->occupied);
Sema_wait(&b->cmut);
Item=b->buf[b->nextout];
b->nextout++;
b->nextout %=BSIZE;
sema_post (&b->cmut);
sema_post(&b->empty):
return(item);
}
3.5进程间同步
四种同步原语中的任何一种都能做进程间的同步。只要保证同步变量在共享内存 段,并且带USYNC_PROCESS参数来对其进行初始化。在这之后,对同步变量的使用和 USYNC_THREAD初始化后的线程同步是一样的。
Mutex_init(&m, USYNC_PROCESS,0);
Rwlock_init(&rw, USYNC_PROCESS,0);
Cond_init(&cv,USYNC_PROCESS,0);
Sema_init(&s,count,USYNC_PROCESS,0);
示例3-18显示了一个生产者/消费者问题,生产者和消费者在两个不同的进程里。 主函数把全零的内存段映射到它的地址空间里。注意mutex_init()和cond_init()一 定要用type=USYNC_PROCESS来初始化。
子进程运行消费者,父进程运行生产者。
此例也显示了生产者和消费者的驱动程序。生产者驱动producer_driver()简单 地从stdin中读字符并且调用生产者函数producer()。消费者驱动consumer_driver() 通过调用consumer()来读取字符,并将其写入stdout。
Code Example 3-18 生产者/消费者问题,用USYNC_PROCESS
Main(){
Int zfd;
Buffer_t * buffer;
Zfd=open("/dev/zero", O_RDWR);
Buffer=(buffer_t *)mmap(NULL, sizeof(buffer_t),
PROT_READ|PROT_WRITE, MAP_SHARED, zfd, 0);
Buffer->occupied=buffer->nextin=buffer->nextout=0;
Mutex_init(&buffer->lock, USYNC_PROCESS,0);
Cond_init(&buffer->less, USYNC_PROCESS, 0);
Cond_init(&buffer->more, USYNC_PROCESS, 0);
If(fork()==0)
Consumer_driver(buffer);
Else
Producer_driver(buffer);
}
void producer_driver(buffer_t *b){
int item;
while(1){
item=getchar();
if(item==EOF){
producer(b, '');
break;
} else
producer(b, (char)item);
}
}
void consumer_driver(buffer_t *b){
char item;
while (1) {
if ((item=consumer(b))=='')
break;
putchar(item);
}
}
一个子进程被创建出来运行消费者;父进程运行生产者。
3.6同步原语的比较
Solaris中最基本的同步原语是互斥锁。所以,在内存使用和执行时它是最 有效的。对互斥锁最基本的使用是对资源的依次访问。
在Solaris中效率排第二的是条件变量。条件变量的基本用法是关于一个状态 的改变而阻塞。在关于一个条件变量阻塞之前一定要先获得互斥锁,在从 cond_wait()返回且改变变量状态后一定要释放该互斥锁。
信号量比条件变量占用更多的内存。因为信号量是作用于状态,而不是控制 ???,所以在一些特定的条件下它更容易使用。和锁不同,信号量没有一个所 有者。任何线程都可以给已阻塞的信号量增值。
读写锁是Solaris里最复杂的同步机制。这意味着它不象其他原语那样细致 ???。一个读写锁通常用在读操作比写操作频繁的时候。
多线程编程指南1--线程基础
线程编程指南1--线程基础
Wednesday, 29. March 2006, 11:48:45
本文出自:BBS水木清华站 作者:Mccartney (coolcat) (2002-01-29 20:25:25)
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(可以跟踪一个程序的系统调用和信号), 可以很好地报告线程的状态。
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.
多线程编程指南2--用多线程编程
Wednesday, 29. March 2006, 11:50:40
本文出自:BBS水木清华站 作者:Mccartney (coolcat) (2002-01-29 20:26:32)
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
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时,错误检查将被执行,没有实际的信号被发送。这可以用来检测arget_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,"%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
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和调度等级"。
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
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时,错误检查将被执行,没有实际的信号被发送。这可以用来检测arget_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,"%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
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和调度等级"。
3.
多线程编程指南3--使用同步对象编程
Wednesday, 29. March 2006, 11:52:42
本文出自:BBS水木清华站 作者:Mccartney (coolcat) (2002-01-29 20:28:07)3 使用同步对象来编程
本章定义了四种可用的同步类型,并且讨论实现同步的注意事项。
互斥锁(mutex)
条件变量(condition variable)
多读单写锁(multi-read,single-write lock)
信号量(semophore)
进程间同步(process synchronization)
同步原语的比较(compare primitive)
同步对象是内存中的变量,你可以象访问一般的数据那样来访问它。不同进程内的线程可以通过共享内存中的同步变量来同步,即使这些线程互不可见。
同步变量可以放置在文件当中,可以比创建它的进程拥有更长的生命。
同步对象的类型包括:
· 互斥锁
· 状态变量
· 读写锁
· 信号灯(信号量)
在下面几种情况下,同步是重要的:
· 在两个或更多个进程内的线程可以合用一个同步变量。注意,同步变量应当被一个进程初始化,在第二次初始化时,该同步变量被设置为解锁状态。
· 同步是唯一保证共享数据持久的办法。
· 一个进程可以映射一个文件并通过一个线程将其加锁,修改完成之后,该线程释放文件锁并恢复文件。在文件加锁的过程中,任何程序中的任何 线程想要加锁时都会阻塞,直至解锁;
· 同步可以保证易变数据的安全。
· 同步对于简单变量也是很重要的,例如整数。在整数没有和总线对齐或
大于数据宽度的情况下,读写一个整数可能需要多个内存周期。虽然在SPARC系统上不会发生这样的情况,但移植程序时不能不考虑这一点;
3.1互斥锁
用互斥锁可以使线程顺序执行。互斥锁通常只允许一个线程执行一个关键部分的代码,来同步线程。互斥锁也可以用来保护单线程代码。
Table 3-1 互斥锁函数
函数 操作
Mutex_init(3T) 初始化一个互斥锁
Mutext_lock(3T) 给一个互斥锁加锁
Mutex_trylock(3T) 加锁,如失败不阻塞
Mutex_unlock(3T) 解锁
Mutex_destroy(3T) 解除互斥状态
如果两个进程有共享且可写的内存,且做了相应的初始化设置后(参见mmap(2)),互斥锁可以实现进程间的线程同步。
互斥锁在使用前一定要初始化。
多线程等待一个互斥锁时,其获得互斥锁的顺序是不确定的。
3.1.1初始化一个互斥锁
mutex_init(3T)
#include ( or #include )
int mutex_init(mutex_t *mp, int type, void * arg);
用mutex_init()来初始化一个由mp指向的互斥锁。Type可以是以下值之一(arg现在先不谈)。
USYNC_PROCESS 互斥锁用来同步进程间的线程。
USYNC_THREAD 互斥锁只用来同步进程内部的线程。
互斥锁也可以通过分配零内存来初始化,在此种情况下应当设定USYNC_THREAD。
一定不会有多个线程同时初始化同一个互斥锁。一个互斥锁在使用期间一定不会被重新初始化。
返回值--mutex_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp或者arg指向一个非法地址。
3.1.2给互斥锁加锁
mutex_lock(3T)
#include (or #include )
int mutex_lock(mutex_t *mp);
用mutex_lock()锁住mp指向的互斥锁。如果mutex已经被锁,当前调用线程阻塞直到互斥锁被其他线程释放(阻塞线程按照线程优先级等待)。当mutex_lock()返回,说明互斥锁已经被当前线程成功加锁。
返回值--mutex_lock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.1.3加非阻塞互斥锁
mutex_trylock(3T)
#include (or #include )
int mutex_trylock(mutex_t *mp);
用mutex_trylock()来尝试给mp指向的互斥锁加锁。这个函数是mutex_lock()的非阻塞版本。当一个互斥锁已经被锁,本调用返回错误。否则,互斥锁被调用者加锁。
返回值--mutex_trylock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
EBUSY mp指向的互斥锁已经被锁。
3.1.4给互斥锁解锁
mutex_unlock(3T)
#include (or #include )
int mutex_unlock(mutex_t *mp);
用mutex_unlock()给由mp指向的互斥锁解锁。互斥锁必须处于加锁状态且调用本函数的线程必须是给互斥锁加锁的线程。如果有其他线程在等待互斥锁,在等待队列头上的线程获得互斥锁并脱离阻塞状态。
返回值--mutex_unlock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.1.5清除互斥锁
mutex_destroy(3T)
#include (or #include )
int mutex_destroy(mutex_t *mp);
用mutex_destroy()函数解除由mp指向的互斥锁的任何状态。储存互斥锁的内存不被释放。
返回值--mutex_destroy()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.1.6互斥锁代码示例
Code Example 3-1 Mutex Lock Example
Mutex_t count_mutex;
Int count;
Increment_count()
{ mutex_lock(&count_mutex);
count=count+1;
mutex_unlock(&cout_mutex);
}
int get_count()
{ int c;
mutex_lock(&count_mutex);
c=count;
mutex_unlock(&count_mutex);
return(c);
}
在示例3-1中两个函数用互斥锁实现不同的功能,increment_count()保证对共享变量的一个原子操作(即该操作不可中断),get_count()用互斥锁保证读取count期间其值不变。
*为锁设置等级
你可能会需要同时访问两种资源。也许你在用其中一种资源时,发现需要另外一 种。就象我们在示例3-2中看到的,如果两个线程希望占有两种资源,但加互斥锁的 顺序不同,有可能会发生问题。在这个例子当中,两个线程分别给互斥锁1和2加锁, 在它们想给另外的资源加锁的时候,将会发生死锁。
Code Example 3-2 Deadlock
Thread 1:
Mutex_lock(&m1)
/* use resource 1*/
mutex_lock(&m2);
/* use resources 1 and 2*/
mutex_unlock(&m2);
mutex_unlock(&m1);
Thread 2:
Mutex_lock(&m2);
/*use resource 2*/
mutex_lock(&m1);
/* use resources 1 and 2*/
mutex_unlock(&m1);
mutex_unlock(&m2);
避免这个问题的最好办法是在线程给多个互斥锁加锁时,遵循相同的顺序。这种技术的一种实现叫"锁的等级":在逻辑上为每个锁分配一个数进行排序。
如果你已经拥有一个等级为I的互斥锁,你将不能给等级小于I的互斥锁加锁。
---------------------------------------
注意--lock_init可以检测这个例子当中死锁的类型。避免死锁的最好办法是采用等
级锁:如果对互斥锁的操作遵循一个预先定义的顺序,死锁将不会发生。
---------------------------------------
但是,这种技术并非总可以使用--有时你必须对互斥锁进行不按照预定义顺序的 操作。为了在这种情况下阻止死锁,一个线程在发现死锁用其他方法无法避免时, 必须释放已经占有的所有资源。示例3-3显示了这种方法。
Code Example 3-3 条件锁
Thread 1:
Mutex_lock(&m1);
Mutex_lock(&m2);
Mutex_unlock(&m2);
Mutex_unlock(&m1);
Thread 2:
For(;{
Mutex_lock(&m2);
If(mutex_trylock(&m1)==0)
/*got it*/
break;
/*didn't get it */
mutex_unlock(&m1);
}
mutex_unlock(&m1);
mutex_unlock(&m2);
在上例中,线程1按照预定的顺序加锁,但线程2打乱了次序。为避免死锁,线程2必须小心操作互斥锁1:如果设置在等待互斥锁释放时阻塞,则可能导致死锁。
为保证上述情况不会发生,线程2调用mutex_trylock,如果互斥锁可用则用, 不可用则立刻返回失败。在这个例子当中,线程2一定要释放互斥锁2,以便线程1 可以使用互斥锁1和互斥锁2。
3.1.7锁内嵌于单链表当中
示例3-4同时占有3个锁,通过锁等级定义避免死锁。
Code Example 3-4 单链表结构
Typedef struct node1{
Int value;
Struct node1 *link;
Mutex_t lock;
}node1_t;
node1_t Listhead;
此例利用单链表结构的每一个节点存储一个互斥锁。为了删除一个互斥锁,要从listhead开始搜索(它本身不会被删除),知道找到指定的节点。
为了保证同时删除不会发生,在访问其内容之前要先锁定节点。因为所有的搜索从listhead开始按顺序进行,所以不会出现死锁。
如果找到指定节点,对该节点和其前序节点加锁,因为两个节点都需要改变。因为前序节点总是首先加锁,死锁将不会发生。
下面C程序从单链表中删除一项。
Code Example 3-5 内嵌锁的单链表
Node1_t * delete(int value){
Node1_t * prev, *current;
Prev =&listhead;
Mutex_lock(&prev->lock);
While((current=prev->link)!=NULL){
Mutex_lock(¤t->lock);
If(current->value==value){
Prev->link=current->link;
Mutex_unlock(¤t->lock);
Mutex_unlock(&prev->lock);
Current->link=NULL;
Return(current);
}
mutex_unlock(&prev->lock);
prev=current;
}
mutex_unlock(&prev->lock);
return(NULL);
}
3.1.8内嵌在环状链表中的锁
示例3-6把前例的单链表改为环链表。环链表没有显式的表头;一个线程可以和某个节点连接,对该节点及其邻节点进行操作。等级锁在这里不容易使用,因为其链表是环状的。
Code Example 3-6 Circular Linked List Structure
Typedef struct node 2 {
Int value;
Struct node2 *link;
Mutex_t lock;
} node2_t;
下面的C程序给两个节点加锁,并对它们做操作。
Code Example 3-7 内嵌锁的环链表
Void Hit Neighbor(node2_t *me){
While(1){
Mutex_lock(&me->lock);
If(mutex_lock(&me->link->lock)){
/* failed to get lock*/
mutex_unlock(&me->lock);
continue;
}
break;
}
me->link->value += me->value;
me->value /=2;
mutex_unlock(&me->link->lock);
mutex_unlock(&me->lock);
}
3.2条件变量
用条件变量来自动阻塞一个线程,直到某特殊情况发生。通常条件变量和互斥锁同时使用。
Table3-2 有关条件变量的函数
函数 操作
Cond_init(3T) 初始化条件变量
Cond_wait(3T) 基于条件变量阻塞
Cond_signal(3T) 解除指定线程的阻塞
Cond_timedwait(3T) 阻塞直到指定事件发生
Cond_broadcast(3T) 解除所有线程的阻塞
Cond_destroy(3T) 破坏条件变量
通过条件变量,一个线程可以自动阻塞,直到一个特定条件发生。条件的检测是在互斥锁的保护下进行的。
如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如 果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它 的线程,重新获得互斥锁,重新评价条件。
如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。
使用条件变量之前要先进行初始化。而且,在有多个线程等待条件变量时,它们解除阻塞不存在确定的顺序。
3.2.1初始化条件变量
cond_init(3T)
#include (or #include )
int cond_init(cond_t *cvp, int type, int arg);
用cond_init()初始化有cvp指向的条件变量。Type可以是如下值之一(arg先
不谈):
USYNC_PROCESS 条件变量可以在进程间实现线程同步;
USYNC_THREAD 条件变量只能在进程内部对线程同步;
条件变量可以用分配零内存来初始化,在这种情况下一定要是USYNC_THREAD。
多线程不能同时初始化同一个条件变量。如果一个条件变量正在使用,它不能被重新初始化。
返回值--cond_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数
EFAULT mp指向一个非法地址。
3.2.2关于条件变量阻塞
cond_wait(3T)
#include (or #include )
int cond_wait(cond_t *cvp, mutex_t *mp);
用cond_wait()释放由mp 指向的互斥锁,并且使调用线程关于cvp指向的条件 变量阻塞。被阻塞的线程可以被cond_signal(), cond_broadcast(),或者由fork() 和传递信号引起的中断唤醒。
与条件变量关联的条件值的改变不能从cond_wait()的返回值得出,这样的状 态必须被重新估价。
即使是返回错误信息,Cond_wait()通常在互斥锁被调用线程加锁后返回。
函数阻塞直到条件被信号唤醒。它在阻塞前自动释放互斥锁,在返回前在自动 获得它。
在一个典型的应用当中,一个条件表达式在互斥锁的保护下求值。如果条件表 达式为假,线程基于条件变量阻塞。当一个线程改变条件变量的值时,条件变量获 得一个信号。这使得等待该条件变量的一个或多个线程退出阻塞状态,并试图得到 互斥锁。
因为在被唤醒的线程的cond_wait()函数返回之前条件已经改变,导致等待的 条件在得到互斥锁之前必须重新测试。推荐的办法是在while循环中写条件检查。
Mutex_lock();
While(condition_is_false)
Cond_wait();
Mutes_unlock();
如果有多个线程关于条件变量阻塞,其退出阻塞状态的顺序不确定。
返回值--cond_wait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
EINTR 等待被信号或fork()中断。
3.2.3使指定线程退出阻塞状态
cond_signal(3T)
#include (or #include )
int cond_signal (cond_t *cvp);
用cond_signal()使得关于由cvp指向的条件变量阻塞的线程退出阻塞状态。在 同一个互斥锁的保护下使用cond_signal()。否则,条件变量可以在对关联条件变量 的测试和cond_wait()带来的阻塞之间获得信号,这将导致无限期的等待。
如果没有一个线程关于条件变量阻塞,cond_signal无效。
返回值--cond_signal()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
Code Example 3-8 使用cond_wait(3T)和cond_signal(3T)的例子
Mutex_t count_lock;
Cond_t count_nonzero;
Unsigned int count;
Decrement_count()
{
mutex_lock(&count_lock);
while(count==0)
cond_wait(&count_nonzero,&count_lock);
count=count-1;
mutex_unlock(&count_lock);
}
increment_count()
{
mutex_lock(&count_lock);
if(count==0)
cond_signal(&count_nonzero);
count=count+1;
mutex_unlock(&count_lock);
}
3.2.4阻塞直到指定事件发生
cond_timedwait(3T)
#include (or #include )
int cond_timedwait(cond_t *cvp, mutex_t *mp,
timestruc_t *abstime);
cond_timedwait()和cond_wait()用法相似,差别在于cond_timedwait()在经过有abstime指定的时间时不阻塞。
即使是返回错误,cond_timedwait()也只在给互斥锁加锁后返回。
Cond_timedwait()函数阻塞,直到条件变量获得信号或者经过由abstime指定 的时间。Time-out被指定为一天中的某个时间,这样条件可以在不重新计算 time-out值的情况下被有效地重新测试,???就象在示例3-9中那样。
返回值--cond_timedwait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 由abstime 指定的时间大于应用程序启动的时间加50,000,000,或者纳秒数大于等于1,000,000,000。
EFAULT cvp指向一个非法地址。
EINTR 等待被信号或fork()中断。
ETIME abstime指定的时间已过。
Code Example 3-9 时间条件等待
Timestruc_t to;
Mutex_t m;
Cond_t c;
Mutex_lock(&m);
To.tv_sec=time(NULL)+TIMEOUT;
To.tv_nsec=0;
While (cond==FALSE){
Err=cond_timedwait(&c,&m,&to);
If(err=ETIME) {
/* TIMEOUT, do something */
break;
}
}
mutex_unlock(&m);
3.2.5使所有线程退出阻塞状态
cond_broadcast(3T)
#include ( or #include )
int cond_wait(cond_t *cvp);
用cond_broadcast()使得所有关于由cvp指向的条件变量阻塞的线程退出阻塞状态。如果没有阻塞的线程,cond_broadcast()无效。
这个函数唤醒所有由cond_wait()阻塞的线程。因为所有关于条件变量阻塞的线程都同时参与竞争,所以使用这个函数需要小心。
例如,用cond_broadcast()使得线程竞争变量资源,如示例3-10所示。
Code Example 3-10 条件变量广播
Mutex_t rsrc_lock;
Cond_t rsrc_add;
Unsigned int resources;
Get_resources(int amount)
{ mutex_lock(&rsrc_lock);
while(resources < amount) {
cond_wait(&rsrc_add, &rsrc_lock);
}
resources-=amount;
mutex_unlock(&rsrc_lock);
}
add_resources(int amount)
{
mutex_lock(&rsrc_lock);
resources +=amount;
cond_broadcast(&rsrc_add);
mutex_unlock(&rsrc_lock);
}
注意,在互斥锁的保护内部,首先调用cond_broadcast()或者首先给resource增值,效果是一样的。
返回值--cond_broadcast()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
在互斥锁的保护下调用cond_broadcast()。否则,条件变量可能在检验关联状态和通过cond_wait()之间获得信号,这将导致永久等待。
3.2.6清除条件变量
cond_destroy(3T)
#include ( or #include )
int cond_destroy(cond_t *cvp);
使用cond_destroy() 破坏由cvp指向的条件变量的任何状态。但是储存条件变量的空间将不被释放。
返回值--cond_destroy()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EFAULT cvp指向一个非法地址。
3.2.7唤醒丢失问题
在没有互斥锁保护的情况下调用cond_signal()或者cond_broadcast()会导致丢 失唤醒问题。一个唤醒丢失发生在信号或广播已经发出,但是线程即使在条件为真 时仍然关于条件变量阻塞,具体地说,这发生在调用cond_signal()时并没有获得互 斥锁的情况下。
如果一个线程已经作过条件检验,但是尚未调用cond_wait(),这时另外一个线 程调用cond_signal(),因为没有已被阻塞的线程,唤醒信号丢失。
3.2.8生产者/消费者问题
这个问题是一个标准的、著名的同时性编程问题的集合:一个有限缓冲区和两类线程,生产者和消费者,他们分别把产品放入缓冲区和从缓冲区中拿走产品。
一个生产者在缓冲区满时必须等待,消费者在缓冲区空时必须等待。
一个条件变量代表了一个等待条件的线程队列。
示例3-11有两个队列,一个(less)给生产者,它们等待空的位置以便放入信 息;另外一个(more)给消费者,它们等待信息放入缓冲区。这个例子也有一个互 斥锁,它是一个结构,保证同时只有一个线程可以访问缓冲区。
下面是缓冲区数据结构的代码。
Code Example 3-11 生产者/消费者问题和条件变量
Typedef struct{
Char buf[BSIZE];
Int occupled;
Int nextin;
Int nextout;
Mutex_t mutex;
Cond_t more;
Cond_t less;
}buffer_t;
buffer_t buffer;
如示例3-12所示,生产者用一个互斥锁保护缓冲区数据结构然后确定有足够的空 间来存放信息。如果没有,它调用cond_wait(),加入关于条件变量less阻塞的线程 队列,说明缓冲区已满。这个队列需要被信号唤醒。
同时,作为cond_wait()的一部分,线程释放互斥锁。等待的生产者线程依赖于 消费者线程来唤醒。当条件变量获得信号,等待less的线程队列里的第一个线程被唤 醒。但是,在线程从cond_wait()返回前,必须获得互斥锁。
这再次保证了线程获得对缓冲区的唯一访问权。线程一定要检测缓冲区有足够的 空间,如果有的话,它把信息放入下一个可用的位置里。
同时,消费者线程也许正在等待有信息放入缓冲区。这些线程等待条件变量more。 一个生产者线程,在刚刚把信息放入存储区后,调用cond_signal()来唤醒下一个等 待的消费者。(如果没有等待的消费者,这个调用无效。)最后,生产者线程释放互 斥锁,允许其他线程操作缓冲区。
Code Example 3-12 生产者/消费者问题--生产者
Void producer(buffer_t *b, char item) {
Mutex_lock(&b->mutex);
While ( b->occupied >= BSIZE)
Cond_wait(&b->less, &b->mutex);
Assert(b->occupied < BSIZE);
b->buf(b->nextin++)=item;
b->nextin %=BSIZE;
b->occupied ++;
/* now: either b->occupied < BSIZE and b->nextin is the index
of the next empty slot in the buffer, or
b->occupied == BSIZE and b->nextin is the index of the
next (occupied) slot that will be emptied by a consumer
(such as b-> == b->nextout) */
cond_signal(&b->more);
mutex_unlock(&b->mutex);
}
注意assert()命令的用法;除非代码用NDEBUG方式编译,assert()在参数为真时 (非零值)不做任何操作,如果参数为假(参数为假),程序退出。
这种声明在多线程编程中特别有用--在失败时它们会立刻指出运行时的问题, 它们还有其他有用的特性。
后面说明代码可以更加称得上是声明,但它太过复杂,无法用布尔表达式来表达,所以用文字来写。???
声明和说明???都是不变量的实例。它们都是一些逻辑命题,在程序正常执行时不应当被证伪,除非一个线程试图改变非变量说明段的变量。???
不变量是一种极为有用的技术。即使它们没有在程序中写出,在分析程序中也需要把它们看成不变量。
生产者代码中的不变量(说明部分)在程序执行到这一段时一定为真。如果你把这段说明移到mutex_unlock()后面,它将不一定保持为真。如果将其移到紧跟着声明的后面,它仍然为真。
关键在于,不变量表现了一个始终为真的属性,除非一个生产者或一个消费者正 在改变缓冲区的状态。如果一个线程正在操作缓冲区(在互斥锁的保护下),它将暂 时将不变量置为假。但是,一旦线程结束对缓冲区的操作,不变量会立刻恢复为真。
示例3-13为消费者的代码。它的流程和生产者是对称的。
Code Example 3-13 生产者/消费者问题--消费者
Char consumer(buffer_t *b){
Char item;
Mutex_lock(&b->mutex);
While(b->occupied <=0)
Cond_wait(&b->more, &b->mutex);
Assert(b->occupied>0);
Item=b->buf(b->nextout++);
b->nextout %=BSIZE;
b->occupied--;
/* now: either b->occupied>0 and b->nextout is the index of
the nexto ccupied slot in the buffer, or b->occupied==0
and b->nextout is the index of the next(empty) slot that
will be filled by a producer (such as b->nextout ==b->nextin) */
cond_signal(&b->less);
mutex_unlock(&b->mutex);
return(item);
}
3.3多读单写锁
读写锁允许多个线程同时进行读操作,但一个时间至多只有一个线程进行写操作。
表3-3 读写锁的函数
函数 操作
rwlock_init(3T) 初始化一个读写锁
rw_rdlock(3T) 获得一个读锁
rw_tryrdlock(3T) 试图获得一个读锁
rw_wrlock(3T) 获得一个写锁
rw_trywrlock(3T) 试图获得一个写锁
rw_unlock(3T) 使一个读写锁退出阻塞
rwlock_destroy(3T) 清除读写锁状态
如果任何线程拥有一个读锁,其他线程也可以拥有读锁,但必须等待写锁。如 果一个线程拥有写锁,或者正在等待获得写锁,其它线程必须等待获得读锁或写锁。
读写锁比互斥锁要慢,但是在所保护的数据被频繁地读但并不频繁写的时候可以提高效率。
如果两个进程有共享的可读写的内存,可以在初始化时设置成用读写锁进行进程间的线程同步。
读写锁使用前一定要初始化。
3.3.1初始化一个读写锁
rwlock_init(3T)
#include (or #include )
int rwlock_init(rwlock_t *rwlp, int type, void * arg);
用rwlock_init()来初始化由rwlp指向的读写锁并且设置锁的状态为没有锁。
Type可以是如下值之一(arg现在先不谈)。
USYNC_PROCESS 读写锁可以实现进程间的线程同步。
USYNC_THREAD 读写锁只能在进程内部实现线程同步。
多线程不能同时初始化一个读写锁。读写锁可以通过分配零内存来初始化,在这种情况下,一定要设置USYNC_THREAD。一个读写锁在使用当中不能被其他线程重新初始化。
返回值--rwlock_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp或arg指向一个非法地址。
3.3.2获得一个读锁
rw_rdlock(3T)
#include (or #include )
int rw_rdlock(rwlock_t *rwlp);
用rw_rdlock()来给一个由rwlp指向的读写锁加上读锁。如果读写锁已经被加写锁,则调用线程阻塞直到写锁被释放。否则,读锁将被成功获得。
返回值--rw_rdlock()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
3.3.3试图获得一个读锁
rw_tryrdlock(3T)
#include (or #include )
int rw_tryrdlock(rwlock_t *rwlp);
试图给读写锁加读锁,如果读写锁已经被加写锁,则返回错误,而不再进入阻塞状态。否则,读锁将被成功获得。
返回值--rw_tryrdlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
EBUSY 由rwlp指向的读写锁已经被加写锁。
3.3.4获得一个写锁
rw_wrlock(3T)
#include (or #include )
int rw_wrlock(rwlock_t *rwlp);
用rw_wrlock()为由rwlp指向的读写锁加写锁。如果该读写锁已经被加读锁或写锁,则调用线程阻塞,直到所有锁被释放。一个时刻只有一个线程可以获得写锁。
返回值--rw_wrlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
3.3.5试图获得写锁
rw_trywrlock(3T)
#include (or #include )
int rw_trywrlock(rwlock_t *rwlp);
用rw_trywrlock()试图获得写锁,如果该读写锁已经被加读锁或写锁,它将返回错误。
返回值--rw_trywrlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
EBUSY 由rwlp指向的读写锁已被加锁。
3.3.6使一个读写锁退出阻塞状态
rw_unlock(3T)
#include (or #include )
int rwlock_tryrdlock(rwlock_t *rwlp);
用rw_unlock()来使由rwlp指向的读写锁退出阻塞状态。调用线程必须已经获得对该读写锁的读锁或写锁。如果任何其它线程在等待读写锁可用,它们当中的一个将退出阻塞状态。
返回值--rw_unlock ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
3.3.7清除读写锁
rwlock_destroy(3T)
#include (or #include )
int rwlock_destroy(rwlock_t *rwlp);
使用rwlock_destroy()来取消由rwlp指向的读写锁的状态。存储读写锁的空间不被释放。
返回值--rw_destroy ()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT rwlp指向一个非法地址。
示例3-14用一个银行帐户来演示读写锁。如果一个程序允许多个线程同时进行读操作,一个时刻只有一个写操作被允许。注意get_balance()函数通过锁来保证检查和储存操作是原子操作。
Code Example 3-14 读/写银行帐户
Rwlock_t account_lock;
Float checking_balance=100.0;
Float saving_balance=100.0;
… …
rwlock_init (&account_lock, 0, NULL);
… …
float get_balance(){
float bal;
rw_rdlock(&account_lock);
bal=checking_balance +saving_balance;
rw_unlock(&account_lock);
return(bal);
}
void tranfer_checking_to_savings(float amount) {
rw_wrlock(&account_lock);
checking_balance=checking_balance - amount;
savings_balance=savings_balance +amount;
rw_unlock(&account_lock);
}
3.4信号量(信号灯)
信号灯是E.W.Dijkstra在60年代晚期定义的程序结构。Dijkstra的模型是一个铁路上的操作:一段单线铁路在一个时刻只允许一列火车通过。
用一个信号灯来维护这段铁路。一列火车在进入单线铁路之前必须等待信号灯 的许可。如果一列火车进入这段轨道,信号灯改变状态,以防止其他火车进入。在 火车离开这段轨道时,必须将信号灯复原,使得其他火车得以进入。
在信号灯的计算机版本中,一个信号灯一般是一个整数,称之为信号量。一个 线程在被允许进行后对信号量做一个p操作。
P操作的字面意思是线程必须等到信号量的值为正(positive)才能继续进行, 进行前先给信号量减1。当做完相关的操作时(相当于离开铁轨),线程执行一个 v操作,即给信号量加1。这两个操作必须具有不可中断性,也叫不可分性,英文字 面为原子性(atomic),即他们不能被分成两个子操作,在子操作之间还可以插入 其它线程的其他操作,这些操作可能改变信号量。在P操作中,信号量的值在被减之 前一定要为正(使得信号量在被减1之后不会为负)。
在P操作或V操作当中,操作不会互相干扰。如果两个V操作要同时执行,则信号量的新值比原来大2。
记住P和V本身是什么意思已经不重要了,就象记住Dijkstra是荷兰人一样。但 是,如果引起了学者考证的兴趣,P代表prolagen,一个由proberen de verlagen演 变来的合成词,它的意思是"试图减"。V代表verhogen,它的意思是"增加"。这些在 Dijkstra的技术笔记EWD 74中提到过。
Sema_wait(3T)和sema_post(3T)分别对应Dijkstra的P和V操作, sema_trywait(3T)是P操作的一个可选的形式,在P操作不能执行时,线程不会阻塞, 而是立刻返回一个非零值。
有两种基本的信号量:二值信号量,其值只能是0或者1,和计数信号量,可以 是非负值。一个二值信号量在逻辑上相当于一个互斥锁。
然而,尽管并不强制,互斥锁应当被认为只能被拥有锁的线程释放,而"拥有信 号量的线程"这个概念是不存在的,任何线程都可以进行一个V操作 (或sema_post(3T))。
计数信号量的功能大概和与互斥锁合用的条件变量一样强大。在很多情况下, 采用信号量的程序比采用条件变量要简单一些(如下面的例子所示)。
然而,如果一个互斥锁和条件变量一起使用,有一个隐含的框架,程序的哪一 部分被保护是明显的。在信号量则不然,它可以用同时性编程当中的go to 来调用, 它更适合用于那些结构性不强的,不精确的方面。
3.4.1计数信号量
在概念上,一个信号量是一个非负整数。信号量在典型情况下用来协调资源, 信号量一般被初始化为可用资源的数量。线程在假如资源是给计数器加1,在拿走资 源时给计数器减1,操作都具有原子性。
如果一个信号量的值变为0,表明已无可用资源,想要给信号量减1的操作必须 等到它为正时。
表3-4 信号量函数
函数 操作
Sema_init(3T) 初始化信号量
Sema_post(3T) 增加信号量
Sema_wait(3T) 关于信号量阻塞
Sema_trywait(3T) 减少信号量
Sema_destroy(3T) 破坏信号量的状态
因为信号量不被哪个线程占有,它们可以用异步事件来通知(例如信号处理器)。 而且,因为信号量包含状态,他们可以被异步使用???,而不用象条件变量那样 一定要先获得互斥锁。
缺省情况下,等待信号量的多个线程退出阻塞的顺序是不确定的。
信号量在使用前一定要初始化。
3.4.2初始化一个信号量
sema_init(3T)
#include (or #include )
int sema_init(sema_t *sp, unsigned int count, int type, void *arg);
sema_init用count的值来初始化由sp指向的信号量。Type可以是如下值之一(arg先不谈)。
USYNC_PROCESS 信号量可以在进程间进行线程同步。只有一个进程需要初始化
信号量。Arg忽略。
USYNC_THREAD 信号量只能在进程内部进行线程同步。
多个线程不能同时初始化同一个信号量。一个信号量在使用中不能被其他线程重新初始化。
返回值--sema_init()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp或arg指向一个非法地址。
3.4.3给信号量增值
sema_post(3T)
#include (or #include )
int sema_destroy(sema_t *sp);
用sema_post()给由sp指向的信号量原子地(表示其不可分性,下同)增1,如果有其它线程关于信号量阻塞,其中一个退出阻塞状态。
返回值--sema_post()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
3.4.4关于一个信号量阻塞
sema_wait(3T)
#include (or #include )
int sema_wait(sema_t *sp)
用sema_wait()使得调用线程在由sp指向的信号量小于等于零时阻塞,在其大于零原子地对其进行减操作。
返回值--sema_wait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
EINTR 等待被信号或fork()打断。
3.4.5给信号量减值
sema_trywait(3T)
#include (or #include )
int sema_trywait(sema_t *sp)
用sema_trywait()在sp比零大时对它进行原子地减操作。是sema_wait()的非阻塞版本。
返回值--sema_trywait()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
EBUSY sp 指向的值为零。
3.4.6清除信号量的状态
sema_destroy(3T)
#include (or #include )
int sema_destroy(sema_t *sp)
用sema_destroy(3T)破坏与sp指向的信号量关联的任何状态,但空间不被释放。
返回值--sema_destroy()在成功执行后返回零。其他值意味着错误。在以下情况发生时,函数失败并返回相关值。
EINVAL 非法参数。
EFAULT sp指向一个非法地址。
3.4.7用信号量解决生产者/消费者问题
示例3-15所示的程序与条件变量的解决方案类似;两个信号量代表空和满的缓冲区的数目,生产者线程在没有空缓冲区时阻塞,消费者在缓冲区全空时阻塞。
Code Example 3-15 用信号量解决的生产者/消费者问题
Typedef struct{
Char buf[BSIZE];
Sema_t occupied;
Sema_t empty;
Int nextin;
Int nextout;
Sema_t pmut;
Sema_t cmut;
} buffer_t;
buffer_t buffer;
sema_init(&buffer.occupied, 0, USYNC_THREAD, 0);
sema_init(&buffer.empty, BSIZE, USYNC_THREAD, 0);
sema_init(&buffer.pmut, 1, USYNC_THREAD, 0);
sema_init(&buffer.cmut, 1, USYNC_THREAD, 0);
buffer.nextin=buffer.nextout =0;
另外一对信号量与互斥锁作用相同,用来在有多生产者和多个空缓冲区的情况下,或者是有多个消费者和多个满的缓冲区的情况下控制对缓冲区的访问。互斥锁同样可以工作,但这里主要是演示信号量的例子。
Code Example 3-16 生产者/消费者问题--生产者
Void producer(buffer_t *b, char item){
Sema_wait(&b->empty);
Sema_wait(&b->pmut);
b->buf[b->nextin]=item;
b->nextin++;
b->nextin %=BSIZE;
sema_post( &b->pmut);
sema_post(&b->occupied);
}
Code Example 3-17 生产者/消费者问题--消费者
Char consumer(buffer_t *b){
Char item;
Sema_wait(&b->occupied);
Sema_wait(&b->cmut);
Item=b->buf[b->nextout];
b->nextout++;
b->nextout %=BSIZE;
sema_post (&b->cmut);
sema_post(&b->empty):
return(item);
}
3.5进程间同步
四种同步原语中的任何一种都能做进程间的同步。只要保证同步变量在共享内存 段,并且带USYNC_PROCESS参数来对其进行初始化。在这之后,对同步变量的使用和 USYNC_THREAD初始化后的线程同步是一样的。
Mutex_init(&m, USYNC_PROCESS,0);
Rwlock_init(&rw, USYNC_PROCESS,0);
Cond_init(&cv,USYNC_PROCESS,0);
Sema_init(&s,count,USYNC_PROCESS,0);
示例3-18显示了一个生产者/消费者问题,生产者和消费者在两个不同的进程里。 主函数把全零的内存段映射到它的地址空间里。注意mutex_init()和cond_init()一 定要用type=USYNC_PROCESS来初始化。
子进程运行消费者,父进程运行生产者。
此例也显示了生产者和消费者的驱动程序。生产者驱动producer_driver()简单 地从stdin中读字符并且调用生产者函数producer()。消费者驱动consumer_driver() 通过调用consumer()来读取字符,并将其写入stdout。
Code Example 3-18 生产者/消费者问题,用USYNC_PROCESS
Main(){
Int zfd;
Buffer_t * buffer;
Zfd=open("/dev/zero", O_RDWR);
Buffer=(buffer_t *)mmap(NULL, sizeof(buffer_t),
PROT_READ|PROT_WRITE, MAP_SHARED, zfd, 0);
Buffer->occupied=buffer->nextin=buffer->nextout=0;
Mutex_init(&buffer->lock, USYNC_PROCESS,0);
Cond_init(&buffer->less, USYNC_PROCESS, 0);
Cond_init(&buffer->more, USYNC_PROCESS, 0);
If(fork()==0)
Consumer_driver(buffer);
Else
Producer_driver(buffer);
}
void producer_driver(buffer_t *b){
int item;
while(1){
item=getchar();
if(item==EOF){
producer(b, '');
break;
} else
producer(b, (char)item);
}
}
void consumer_driver(buffer_t *b){
char item;
while (1) {
if ((item=consumer(b))=='')
break;
putchar(item);
}
}
一个子进程被创建出来运行消费者;父进程运行生产者。
3.6同步原语的比较
Solaris中最基本的同步原语是互斥锁。所以,在内存使用和执行时它是最 有效的。对互斥锁最基本的使用是对资源的依次访问。
在Solaris中效率排第二的是条件变量。条件变量的基本用法是关于一个状态 的改变而阻塞。在关于一个条件变量阻塞之前一定要先获得互斥锁,在从 cond_wait()返回且改变变量状态后一定要释放该互斥锁。
信号量比条件变量占用更多的内存。因为信号量是作用于状态,而不是控制 ???,所以在一些特定的条件下它更容易使用。和锁不同,信号量没有一个所 有者。任何线程都可以给已阻塞的信号量增值。
读写锁是Solaris里最复杂的同步机制。这意味着它不象其他原语那样细致 ???。一个读写锁通常用在读操作比写操作频繁的时候。