本章定义了四种可用的同步类型,并且讨论实现同步的注意事项。
1、互斥锁(mutex)
2、条件变量(condition variable)
3、多读单写锁(multi-read,single-write lock)
4、信号量(semophore)
5、进程间同步(process synchronization)
6、同步原语的比较(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 内嵌锁的环链表
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; |
另外一对信号量与互斥锁作用相同,用来在有多生产者和多个空缓冲区的情况下,或者是有多个消费者和多个满的缓冲区的情况下控制对缓冲区的访问。互斥锁同样可以工作,但这里主要是演示信号量的例子。
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。
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里最复杂的同步机制。这意味着它不象其他原语那样细致。一个读写锁通常用在读操作比写操作频繁的时候。
|