Linux下面的线程锁,条件变量以及信号量的使用

一) 线程锁
1) 只能用于"锁"住临界代码区域
2) 一个线程加的锁必须由该线程解锁.

锁几乎是我们学习同步时最开始接触到的一个策略,也是最简单, 最直白的策略.

二) 条件变量,与锁不同, 条件变量用于等待某个条件被触发
1) 大体使用的伪码:

// 线程一代码
pthread_mutex_lock(&mutex);
// 设置条件为true
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);

// 线程二代码
pthread_mutex_lock(&mutex);
while (条件为false)
    pthread_cond_wait(&cond, &mutex);
修改该条件
pthread_mutex_unlock(&mutex);

需要注意几点:
1) 第二段代码之所以在pthread_cond_wait外面包含一个while循环不停测试条件是否成立的原因是, 在pthread_cond_wait被唤醒的时候可能该条件已经不成立.UNPV2对这个的描述是:"Notice that when pthread_cond_wait returns, we always test the condition again, because spurious wakeups can occur: a wakeup when the desired condition is still not true.".

2) pthread_cond_wait调用必须和某一个mutex一起调用, 这个mutex是在外部进行加锁的mutex, 在调用pthread_cond_wait时, 内部的实现将首先将这个mutex解锁, 然后等待条件变量被唤醒, 如果没有被唤醒, 该线程将一直休眠, 也就是说, 该线程将一直阻塞在这个pthread_cond_wait调用中, 而当此线程被唤醒时, 将自动将这个mutex加锁.
man文档中对这部分的说明是:
pthread_cond_wait atomically unlocks the mutex (as per pthread_unlock_mutex) and waits for the condition variable cond to  be  signaled.  The thread execution is suspended and does not consume any CPU time until the condition variable is
signaled. The mutex must be locked by the calling thread on entrance to pthread_cond_wait.  Before  returning  to  the calling thread, pthread_cond_wait re-acquires mutex (as per pthread_lock_mutex).
也就是说pthread_cond_wait实际上可以看作是以下几个动作的合体:
解锁线程锁
等待条件为true
加锁线程锁.

这里是使用条件变量的经典例子:
http://www.cppblog.com/CppExplore/archive/2008/03/20/44949.html
之所以使用两个条件变量, 是因为有两种情况需要进行保护,使用数组实现循环队列,因此一个条件是在getq函数中判断读写指针相同且可读数据计数为0,此时队列为空没有数据可读,因此获取新数据的条件变量就一直等待,另一个条件是读写指针相同且可读数据计数大于0,此时队列满了不能再添加数据, 因此添加新数据的条件变量就一直等待,而nEmptyThreadNum和nFullThreadNum则是计数, 只有这个计数大于0时才会唤醒相应的条件变量,这样可以减少调用pthread_cond_signal的次数.
为了在下面的叙述方便, 我将这段代码整理在下面, 是一个可以编译运行的代码,但是注意需要在编译时加上-pthread链接线程库:
#include 
#include 
#include 
#include 

class CThreadQueue
{
public:
    CThreadQueue( int queueSize=1024):
        sizeQueue(queueSize),lput(0),lget(0),nFullThread(0),nEmptyThread(0),nData(0)
    {
        pthread_mutex_init(&mux,0);
        pthread_cond_init(&condGet,0);
        pthread_cond_init(&condPut,0);
        buffer= new  void *[sizeQueue];
    }
     virtual ~CThreadQueue()
    {
        delete[] buffer;
    }
     void * getq()
    {
         void *data;
        pthread_mutex_lock(&mux);
        /*
         此 处循环判断的原因如下:假设2个线程在getq阻塞,然后两者都被激活,而其中一个线程运行比较块,快速消耗了2个数据,另一个线程醒来的时候已经没有新 数据可以消耗了。另一点,man pthread_cond_wait可以看到,该函数可以被信号中断返回,此时返回EINTR。为避免以上任何一点,都必须醒来后再次判断睡眠条件。更 正:pthread_cond_wait是信号安全的系统调用,不会被信号中断。
        */
         while(lget==lput&&nData==0)
        {
            nEmptyThread++;
            pthread_cond_wait(&condGet,&mux);
            nEmptyThread--;     
        }

        data=buffer[lget++];
        nData--;
         if(lget==sizeQueue)
        {
            lget=0;
        }
         if(nFullThread)  // 必要时才进行signal操作,勿总是signal
        {
            pthread_cond_signal(&condPut);    
        }
        pthread_mutex_unlock(&mux);
         return data;
    }
     void putq( void *data)
    {
        pthread_mutex_lock(&mux);
         while(lput==lget&&nData)
        { 
            nFullThread++;
            pthread_cond_wait(&condPut,&mux);
            nFullThread--;
        }
        buffer[lput++]=data;
        nData++;
         if(lput==sizeQueue)
        {
            lput=0;
        }
         if(nEmptyThread)  //必要时才进行signal操作,勿总是signal
        {
            pthread_cond_signal(&condGet);
        }
        pthread_mutex_unlock(&mux);
    }
private:
    pthread_mutex_t mux;
    pthread_cond_t condGet;
    pthread_cond_t condPut;

     void * * buffer;     // 循环消息队列
     int sizeQueue;         // 队列大小
     int lput;         // location put  放数据的指针偏移
     int lget;         // location get  取数据的指针偏移
     int nFullThread;     // 队列满,阻塞在putq处的线程数
     int nEmptyThread;     // 队列空,阻塞在getq处的线程数
     int nData;         // 队列中的消息个数,主要用来判断队列空还是满
};

CThreadQueue queue; // 使用的时候给出稍大的CThreadQueue初始化参数,可以减少进入内核态的操作。

void * produce( void * arg)
{
     int i=0;
    pthread_detach(pthread_self());
     while(i++<100)
    {
        queue.putq(( void *)i);
    }
}

void *consume( void *arg)
{
     int data;
     while(1)
    {
        data=( int)(queue.getq());
        printf("data=%d\n",data);
    }
}

int main()
{    
    pthread_t pid;
     int i=0;

     while(i++<3)
        pthread_create(&pid,0,produce,0);
    i=0;
     while(i++<3)
        pthread_create(&pid,0,consume,0);
    sleep(5);

     return 0;
}


三) 信号量
信号量既可以作为二值计数器(即0,1),也可以作为资源计数器.
主要是两个函数:
sem_wait()  decrements  (locks)  the semaphore pointed to by sem.  If the semaphore's value is greater than zero, then
the decrement proceeds, and the function returns, immediately.  If the semaphore currently has the  value  zero,  then
the  call  blocks  until  either  it  becomes possible to perform the decrement (i.e., the semaphore value rises above
zero), or a signal handler interrupts the call.

sem_post()  increments  (unlocks)  the  semaphore  pointed  to  by sem.  If the semaphore's value consequently becomes
greater than zero, then another process or thread blocked in a sem_wait(3) call will be woken up and proceed  to  lock
the semaphore.

而函数int sem_getvalue(sem_t *sem, int *sval);则用于获取信号量当前的计数.

可以用信号量模拟锁和条件变量:
1) 锁,在同一个线程内同时对某个信号量先调用sem_wait再调用sem_post, 两个函数调用其中的区域就是所要保护的临界区代码了,这个时候其实信号量是作为二值计数器来使用的.不过在此之前要初始化该信号量计数为1,见下面例子中的代码.
2) 条件变量,在某个线程中调用sem_wait, 而在另一个线程中调用sem_post.

我们将上面例子中的线程锁和条件变量都改成用信号量实现以说明信号量如何模拟两者:
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include < string.h>

class CThreadQueue
{
public:
    CThreadQueue( int queueSize=1024):
        sizeQueue(queueSize),lput(0),lget(0),nFullThread(0),nEmptyThread(0),nData(0)
    {
         // pthread_mutex_init(&mux,0);
        mux = sem_open("mutex", O_RDWR | O_CREAT);
         get = sem_open("get", O_RDWR | O_CREAT);
        put = sem_open("put", O_RDWR | O_CREAT);
    
        sem_init(mux, 0, 1);

        buffer= new  void *[sizeQueue];
    }
     virtual ~CThreadQueue()
    {
        delete[] buffer;
        sem_unlink("mutex");
        sem_unlink("get");
        sem_unlink("put");
    }
     void * getq()
    {
         void *data;

         // pthread_mutex_lock(&mux);
        sem_wait(mux);

         while(lget==lput&&nData==0)
        {
            nEmptyThread++;
             // pthread_cond_wait(&condGet,&mux);
            sem_wait( get);
            nEmptyThread--;     
        }

        data=buffer[lget++];
        nData--;
         if(lget==sizeQueue)
        {
            lget=0;
        }
         if(nFullThread)  // 必要时才进行signal操作,勿总是signal
        {
             // pthread_cond_signal(&condPut);    
            sem_post(put);
        }

         // pthread_mutex_unlock(&mux);
        sem_post(mux);

         return data;
    }
     void putq( void *data)
    {
         // pthread_mutex_lock(&mux);
        sem_wait(mux);

         while(lput==lget&&nData)
        { 
            nFullThread++;
             // pthread_cond_wait(&condPut,&mux);
            sem_wait(put);
            nFullThread--;
        }
        buffer[lput++]=data;
        nData++;
         if(lput==sizeQueue)
        {
            lput=0;
        }
         if(nEmptyThread)
        {
             // pthread_cond_signal(&condGet);
            sem_post( get);
        }

         // pthread_mutex_unlock(&mux);
        sem_post(mux);
    }
private:
     // pthread_mutex_t mux;
    sem_t* mux;
     // pthread_cond_t condGet;
    
// pthread_cond_t condPut;
    sem_t*  get;
    sem_t* put;

     void * * buffer;     // 循环消息队列
     int sizeQueue;         // 队列大小
     int lput;         // location put  放数据的指针偏移
     int lget;         // location get  取数据的指针偏移
     int nFullThread;     // 队列满,阻塞在putq处的线程数
     int nEmptyThread;     // 队列空,阻塞在getq处的线程数
     int nData;         // 队列中的消息个数,主要用来判断队列空还是满
};

CThreadQueue queue; // 使用的时候给出稍大的CThreadQueue初始化参数,可以减少进入内核态的操作。

void * produce( void * arg)
{
     int i=0;
    pthread_detach(pthread_self());
     while(i++<100)
    {
        queue.putq(( void *)i);
    }
}

void *consume( void *arg)
{
     int data;
     while(1)
    {
        data=( int)(queue.getq());
        printf("data=%d\n",data);
    }
}

int main()
{    
    pthread_t pid;
     int i=0;

     while(i++<3)
        pthread_create(&pid,0,produce,0);
    i=0;
     while(i++<3)
        pthread_create(&pid,0,consume,0);
    sleep(5);

     return 0;
}


不过, 信号量除了可以作为二值计数器用于模拟线程锁和条件变量之外, 还有比它们更加强大的功能, 信号量可以用做资源计数器, 也就是说初始化信号量的值为某个资源当前可用的数量, 使用了一个之后递减, 归还了一个之后递增, 将前面的例子用资源计数器的形式再次改写如下,注意在初始化的时候要将资源计数进行初始化, 在下面代码中的构造函数中将put初始化为队列的最大数量, 而get为0:
#include 
#include 
#include 
#include 
#include 
#include 
#include 

class CThreadQueue
{
public:
    CThreadQueue( int queueSize=1024):
        sizeQueue(queueSize),lput(0),lget(0)
    {
        pthread_mutex_init(&mux,0);
         get = sem_open("get", O_RDWR | O_CREAT);
        put = sem_open("put", O_RDWR | O_CREAT);

        sem_init( get, 0, 0);
        sem_init(put, 0, sizeQueue);

        buffer= new  void *[sizeQueue];
    }
     virtual ~CThreadQueue()
    {
        sem_unlink("get");
        sem_unlink("put");
        delete[] buffer;
    }
     void * getq()
    {
        sem_wait( get);

         void *data;

        pthread_mutex_lock(&mux);

         /*
        while(lget==lput&&nData==0)
        {
            nEmptyThread++;
            //pthread_cond_wait(&condGet,&mux);
            nEmptyThread--;     
        }
        
*/

        data=buffer[lget++];
         // nData--;
         if(lget==sizeQueue)
        {
            lget=0;
        }
         /*
        if(nFullThread) //必要时才进行signal操作,勿总是signal
        {
            //pthread_cond_signal(&condPut);    
            sem_post(put);
        }
        
*/
        pthread_mutex_unlock(&mux);

        sem_post(put);

         return data;
    }
     void putq( void *data)
    {
        sem_wait(put);

        pthread_mutex_lock(&mux);

         /*
        while(lput==lget&&nData)
        { 
            nFullThread++;
            //pthread_cond_wait(&condPut,&mux);
            sem_wait(put);
            nFullThread--;
        }
        
*/

        buffer[lput++]=data;
         // nData++;
         if(lput==sizeQueue)
        {
            lput=0;
        }
         /*
        if(nEmptyThread)
        {
            //pthread_cond_signal(&condGet);
            sem_post(get);
        }
        
*/

        pthread_mutex_unlock(&mux);

        sem_post( get);
    }
private:
    pthread_mutex_t mux;
     // pthread_cond_t condGet;
    
// pthread_cond_t condPut;
    sem_t*  get;
    sem_t* put;

     void * * buffer;     // 循环消息队列
     int sizeQueue;         // 队列大小
     int lput;         // location put  放数据的指针偏移
     int lget;         // location get  取数据的指针偏移
};

CThreadQueue queue; // 使用的时候给出稍大的CThreadQueue初始化参数,可以减少进入内核态的操作。

void * produce( void * arg)
{
     int i=0;
    pthread_detach(pthread_self());
     while(i++<100)
    {
        queue.putq(( void *)i);
    }
}

void *consume( void *arg)
{
     int data;
     while(1)
    {
        data=( int)(queue.getq());
        printf("data=%d\n",data);
    }
}

int main()
{    
    pthread_t pid;
     int i=0;

     while(i++<3)
        pthread_create(&pid,0,produce,0);
    i=0;
     while(i++<3)
        pthread_create(&pid,0,consume,0);
    sleep(5);

     return 0;
}

可以看见,采用信号量作为资源计数之后, 代码变得"很直白",原来的一些保存队列状态的变量都不再需要了.

信号量与线程锁,条件变量相比还有以下几点不同:
1)锁必须是同一个线程获取以及释放, 否则会死锁.而条件变量和信号量则不必.

2)信号的递增与减少会被系统自动记住, 系统内部有一个计数器实现信号量,不必担心会丢失, 而唤醒一个条件变量时,如果没有相应的线程在等待该条件变量, 这次唤醒将被丢失.

本文转自:http://www.cppblog.com/converse/archive/2009/01/15/72064.html

http://www.ibm.com/developerworks/cn/linux/thread/posix_thread2/index.html

补充条件变量知识:

条件变量函数

 

操作

相关函数说明

初始化条件变量

pthread_cond_init 语法

基于条件变量阻塞

pthread_cond_wait 语法

解除阻塞特定线程

pthread_cond_signal 语法

在指定的时间之前阻塞

pthread_cond_timedwait 语法

在指定的时间间隔内阻塞

pthread_cond_reltimedwait_np 语法

解除阻塞所有线程

pthread_cond_broadcast 语法

销毁条件变量状态

pthread_cond_destroy 语法

初始化条件变量

使用 pthread_cond_init(3C) 可以将 cv 所指示的条件变量初始化为其缺省值,或者指定已经使用 pthread_condattr_init() 设置的条件变量属性。

pthread_cond_init 语法

int pthread_cond_init(pthread_cond_t *cv,

const pthread_condattr_t *cattr);
#include 



pthread_cond_t cv;

pthread_condattr_t cattr;

int ret;



/* initialize a condition variable to its default value */

ret = pthread_cond_init(&cv, NULL);



/* initialize a condition variable */

ret = pthread_cond_init(&cv, &cattr);

cattr 设置为 NULL。将 cattr 设置为 NULL 与传递缺省条件变量属性对象的地址等效,但是没有内存开销。对于 Solaris 线程,请参见cond_init 语法。

使用 PTHREAD_COND_INITIALIZER 宏可以将以静态方式定义的条件变量初始化为其缺省属性。PTHREAD_COND_INITIALIZER 宏与动态分配具有 null 属性的pthread_cond_init() 等效,但是不进行错误检查。

多个线程决不能同时初始化或重新初始化同一个条件变量。如果要重新初始化或销毁某个条件变量,则应用程序必须确保该条件变量未被使用。

pthread_cond_init 返回值

pthread_cond_init() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。

EINVAL

描述:

cattr 指定的值无效。

EBUSY

描述:

条件变量处于使用状态。

EAGAIN

描述:

必要的资源不可用。

ENOMEM

描述:

内存不足,无法初始化条件变量。

基于条件变量阻塞

使用 pthread_cond_wait(3C) 可以以原子方式释放 mp 所指向的互斥锁,并导致调用线程基于 cv 所指向的条件变量阻塞。对于 Solaris 线程,请参见cond_wait 语法。

pthread_cond_wait 语法

int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex);
#include 



pthread_cond_t cv;

pthread_mutex_t mp;

int ret;



/* wait on condition variable */

ret = pthread_cond_wait(&cv, &mp);

阻塞的线程可以通过 pthread_cond_signal() 或 pthread_cond_broadcast() 唤醒,也可以在信号传送将其中断时唤醒。

不能通过 pthread_cond_wait() 的返回值来推断与条件变量相关联的条件的值的任何变化。必须重新评估此类条件。

pthread_cond_wait() 例程每次返回结果时调用线程都会锁定并且拥有互斥锁,即使返回错误时也是如此。

该条件获得信号之前,该函数一直被阻塞。该函数会在被阻塞之前以原子方式释放相关的互斥锁,并在返回之前以原子方式再次获取该互斥锁。

通常,对条件表达式的评估是在互斥锁的保护下进行的。如果条件表达式为假,线程会基于条件变量阻塞。然后,当该线程更改条件值时,另一个线程会针对条件变量发出信号。这种变化会导致所有等待该条件的线程解除阻塞并尝试再次获取互斥锁。

必须重新测试导致等待的条件,然后才能从 pthread_cond_wait() 处继续执行。唤醒的线程重新获取互斥锁并从 pthread_cond_wait() 返回之前,条件可能会发生变化。等待线程可能并未真正唤醒。建议使用的测试方法是,将条件检查编写为调用 pthread_cond_wait() 的 while() 循环。

pthread_mutex_lock();

while(condition_is_false)

pthread_cond_wait();

pthread_mutex_unlock();

如果有多个线程基于该条件变量阻塞,则无法保证按特定的顺序获取互斥锁。


注 –

pthread_cond_wait() 是取消点。如果取消处于暂挂状态,并且调用线程启用了取消功能,则该线程会终止,并在继续持有该锁的情况下开始执行清除处理程序。


pthread_cond_wait 返回值

pthread_cond_wait() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

EINVAL

描述:

cv 或 mp 指定的值无效。

解除阻塞一个线程

对于基于 cv 所指向的条件变量阻塞的线程,使用 pthread_cond_signal(3C) 可以解除阻塞该线程。对于 Solaris 线程,请参见cond_signal 语法。

pthread_cond_signal 语法

int pthread_cond_signal(pthread_cond_t *cv);
#include 



pthread_cond_t cv;

int ret;



/* one condition variable is signaled */

ret = pthread_cond_signal(&cv);

应在互斥锁的保护下修改相关条件,该互斥锁用于获得信号的条件变量中。否则,可能在条件变量的测试和 pthread_cond_wait() 阻塞之间修改该变量,这会导致无限期等待。

调度策略可确定唤醒阻塞线程的顺序。对于 SCHED_OTHER,将按优先级顺序唤醒线程。

如果没有任何线程基于条件变量阻塞,则调用 pthread_cond_signal() 不起作用。


示例 4–8 使用 pthread_cond_wait() 和 pthread_cond_signal()

 

pthread_mutex_t count_lock;

pthread_cond_t count_nonzero;

unsigned count;



decrement_count()

{

pthread_mutex_lock(&count_lock);

while (count == 0)

pthread_cond_wait(&count_nonzero, &count_lock);

count = count - 1;

pthread_mutex_unlock(&count_lock);

}



increment_count()

{

pthread_mutex_lock(&count_lock);

if (count == 0)

pthread_cond_signal(&count_nonzero);

count = count + 1;

pthread_mutex_unlock(&count_lock);

}

pthread_cond_signal 返回值

pthread_cond_signal() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

EINVAL

描述:

cv 指向的地址非法。

说明了如何使用 pthread_cond_wait() 和 pthread_cond_signal()

在指定的时间之前阻塞

pthread_cond_timedwait(3C) 的用法与 pthread_cond_wait() 的用法基本相同,区别在于在由 abstime 指定的时间之后 pthread_cond_timedwait() 不再被阻塞。

pthread_cond_timedwait 语法

int pthread_cond_timedwait(pthread_cond_t *cv,

pthread_mutex_t *mp, const struct timespec *abstime);
#include 

#include



pthread_cond_t cv;

pthread_mutex_t mp;

timestruct_t abstime;

int ret;



/* wait on condition variable */

ret = pthread_cond_timedwait(&cv, &mp, &abstime);

pthread_cond_timewait() 每次返回时调用线程都会锁定并且拥有互斥锁,即使 pthread_cond_timedwait() 返回错误时也是如此。 对于 Solaris 线程

pthread_cond_timedwait() 函数会一直阻塞,直到该条件获得信号,或者最后一个参数所指定的时间已过为止。


注 –

pthread_cond_timedwait() 也是取消点。



示例 4–9 计时条件等待

 

pthread_timestruc_t to;

pthread_mutex_t m;

pthread_cond_t c;

...

pthread_mutex_lock(&m);

to.tv_sec = time(NULL) + TIMEOUT;

to.tv_nsec = 0;

while (cond == FALSE) {

err = pthread_cond_timedwait(&c, &m, &to);

if (err == ETIMEDOUT) {

/* timeout, do something */

break;

}

}

pthread_mutex_unlock(&m);

pthread_cond_timedwait 返回值

pthread_cond_timedwait() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。

EINVAL

描述:

cv 或 abstime 指向的地址非法。

ETIMEDOUT

描述:

abstime 指定的时间已过。

超时会指定为当天时间,以便在不重新计算值的情况下高效地重新测试条件,如示例 4–9 中所示。

在指定的时间间隔内阻塞

pthread_cond_reltimedwait_np(3C) 的用法与 pthread_cond_timedwait() 的用法基本相同,唯一的区别在于 pthread_cond_reltimedwait_np() 会采用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。

pthread_cond_reltimedwait_np 语法

int  pthread_cond_reltimedwait_np(pthread_cond_t *cv, 

pthread_mutex_t *mp,

const struct timespec *reltime);
#include 

#include



pthread_cond_t cv;

pthread_mutex_t mp;

timestruct_t reltime;

int ret;



/* wait on condition variable */

ret = pthread_cond_reltimedwait_np(&cv, &mp, &reltime);

pthread_cond_reltimedwait_np() 每次返回时调用线程都会锁定并且拥有互斥锁,即使 pthread_cond_reltimedwait_np() 返回错误时也是如此。对于 Solaris 线程,请参见 cond_reltimedwait(3C)pthread_cond_reltimedwait_np() 函数会一直阻塞,直到该条件获得信号,或者最后一个参数指定的时间间隔已过为止。


注 –

pthread_cond_reltimedwait_np() 也是取消点。


pthread_cond_reltimedwait_np 返回值

pthread_cond_reltimedwait_np() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。

EINVAL

描述:

cv 或 reltime 指示的地址非法。

ETIMEDOUT

描述:

reltime 指定的时间间隔已过。

解除阻塞所有线程

对于基于 cv 所指向的条件变量阻塞的线程,使用 pthread_cond_broadcast(3C) 可以解除阻塞所有这些线程,这由 pthread_cond_wait() 来指定。

pthread_cond_broadcast 语法

int pthread_cond_broadcast(pthread_cond_t *cv);
#include 



pthread_cond_t cv;

int ret;



/* all condition variables are signaled */

ret = pthread_cond_broadcast(&cv);

如果没有任何线程基于该条件变量阻塞,则调用 pthread_cond_broadcast() 不起作用。对于 Solaris 线程,请参见cond_broadcast 语法。

由于 pthread_cond_broadcast() 会导致所有基于该条件阻塞的线程再次争用互斥锁,因此请谨慎使用 pthread_cond_broadcast()。例如,通过使用pthread_cond_broadcast(),线程可在资源释放后争用不同的资源量,如示例 4–10 中所示。


示例 4–10 条件变量广播

 

pthread_mutex_t rsrc_lock;

pthread_cond_t rsrc_add;

unsigned int resources;



get_resources(int amount)

{

pthread_mutex_lock(&rsrc_lock);

while (resources < amount) {

pthread_cond_wait(&rsrc_add, &rsrc_lock);

}

resources -= amount;

pthread_mutex_unlock(&rsrc_lock);

}



add_resources(int amount)

{

pthread_mutex_lock(&rsrc_lock);

resources += amount;

pthread_cond_broadcast(&rsrc_add);

pthread_mutex_unlock(&rsrc_lock);

}

请注意,在 add_resources() 中,首先更新 resources 还是首先在互斥锁中调用 pthread_cond_broadcast() 无关紧要。

应在互斥锁的保护下修改相关条件,该互斥锁用于获得信号的条件变量中。否则,可能在条件变量的测试和 pthread_cond_wait() 阻塞之间修改该变量,这会导致无限期等待。

pthread_cond_broadcast 返回值

pthread_cond_broadcast() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

EINVAL

描述:

cv 指示的地址非法。

销毁条件变量状态

使用 pthread_cond_destroy(3C) 可以销毁与 cv 所指向的条件变量相关联的任何状态。对于 Solaris 线程,请参见cond_destroy 语法。

pthread_cond_destroy 语法

int pthread_cond_destroy(pthread_cond_t *cv);
#include 



pthread_cond_t cv;

int ret;



/* Condition variable is destroyed */

ret = pthread_cond_destroy(&cv);

请注意,没有释放用来存储条件变量的空间。

pthread_cond_destroy 返回值

pthread_cond_destroy() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下情况,该函数将失败并返回对应的值。

EINVAL

描述:

cv 指定的值无效。

注意:pthread_cond_destroy 销毁一个条件变量,释放它拥有的资源。进入 pthread_cond_destroy 之前,必须没有在该条件变量上等待的线程。
Attempting to destroy a condition variable upon which other threads are currently blocked results in undefined behavior.

唤醒丢失问题

如果线程未持有与条件相关联的互斥锁,则调用 pthread_cond_signal() 或 pthread_cond_broadcast() 会产生唤醒丢失错误。

满足以下所有条件时,即会出现唤醒丢失问题:

  • 一个线程调用 pthread_cond_signal() 或 pthread_cond_broadcast()

  • 另一个线程已经测试了该条件,但是尚未调用 pthread_cond_wait()

  • 没有正在等待的线程

    信号不起作用,因此将会丢失

仅当修改所测试的条件但未持有与之相关联的互斥锁时,才会出现此问题。只要仅在持有关联的互斥锁同时修改所测试的条件,即可调用 pthread_cond_signal() 和pthread_cond_broadcast(),而无论这些函数是否持有关联的互斥锁。

生成方和使用者问题

并发编程中收集了许多标准的众所周知的问题,生成方和使用者问题只是其中的一个问题。此问题涉及到一个大小限定的缓冲区和两类线程(生成方和使用者),生成方将项放入缓冲区中,然后使用者从缓冲区中取走项。

生成方必须在缓冲区中有可用空间之后才能向其中放置内容。使用者必须在生成方向缓冲区中写入之后才能从中提取内容。

条件变量表示一个等待某个条件获得信号的线程队列。

示例 4–11 中包含两个此类队列。一个队列 (less) 针对生成方,用于等待缓冲区中出现空位置。另一个队列 (more) 针对使用者,用于等待从缓冲槽位的空位置中提取其中包含的信息。该示例中还包含一个互斥锁,因为描述该缓冲区的数据结构一次只能由一个线程访问。


示例 4–11 生成方和使用者的条件变量问题

 

typedef struct {

char buf[BSIZE];

int occupied;

int nextin;

int nextout;

pthread_mutex_t mutex;

pthread_cond_t more;

pthread_cond_t less;

} buffer_t;



buffer_t buffer;

如示例 4–12 中所示,生成方线程获取该互斥锁以保护 buffer 数据结构,然后,缓冲区确定是否有空间可用于存放所生成的项。如果没有可用空间,生成方线程会调用pthread_cond_wait()pthread_cond_wait() 会导致生成方线程连接正在等待 less 条件获得信号的线程队列。less 表示缓冲区中的可用空间。

与此同时,在调用 pthread_cond_wait() 的过程中,该线程会释放互斥锁的锁定。正在等待的生成方线程依赖于使用者线程在条件为真时发出信号,如示例 4–12 中所示。该条件获得信号时,将会唤醒等待 less 的第一个线程。但是,该线程必须再次锁定互斥锁,然后才能从 pthread_cond_wait() 返回。

获取互斥锁可确保该线程再次以独占方式访问缓冲区的数据结构。该线程随后必须检查缓冲区中是否确实存在可用空间。如果空间可用,该线程会向下一个可用的空位置中进行写入。

与此同时,使用者线程可能正在等待项出现在缓冲区中。这些线程正在等待条件变量 more。刚在缓冲区中存储内容的生成方线程会调用 pthread_cond_signal() 以唤醒下一个正在等待的使用者。如果没有正在等待的使用者,此调用将不起作用。

最后,生成方线程会解除锁定互斥锁,从而允许其他线程处理缓冲区的数据结构。


示例 4–12 生成方和使用者问题:生成方

 

void producer(buffer_t *b, char item)

{

pthread_mutex_lock(&b->mutex);



while (b->occupied >= BSIZE)

pthread_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->nextin == b->nextout) */



pthread_cond_signal(&b->more);



pthread_mutex_unlock(&b->mutex);

}

请注意 assert() 语句的用法。除非在编译代码时定义了 NDEBUG,否则 assert() 在其参数的计算结果为真(非零)时将不执行任何操作。如果参数的计算结果为假(零),则该程序会中止。在多线程程序中,此类断言特别有用。如果断言失败,assert() 会立即指出运行时问题。assert() 还有另一个作用,即提供有用的注释。

以 /* now: either b->occupied ... 开头的注释最好以断言形式表示,但是由于语句过于复杂,无法用布尔值表达式来表示,因此将用英语表示。

断言和注释都是不变量的示例。这些不变量是逻辑语句,在程序正常执行时不应将其声明为假,除非是线程正在修改不变量中提到的一些程序变量时的短暂修改过程中。当然,只要有线程执行语句,断言就应当为真。

使用不变量是一种极为有用的方法。即使没有在程序文本中声明不变量,在分析程序时也应将其视为不变量。

每次线程执行包含注释的代码时,生成方代码中表示为注释的不变量始终为真。如果将此注释移到紧挨 mutex_unlock() 的后面,则注释不一定仍然为真。如果将此注释移到紧跟 assert() 之后的位置,则注释仍然为真。

因此,不变量可用于表示一个始终为真的属性,除非一个生成方或一个使用者正在更改缓冲区的状态。线程在互斥锁的保护下处理缓冲区时,该线程可能会暂时声明不变量为假。但是,一旦线程结束对缓冲区的操作,不变量即会恢复为真。

示例 4–13 给出了使用者的代码。该逻辑流程与生成方的逻辑流程相对称。


示例 4–13 生成方和使用者问题:使用者

 

char consumer(buffer_t *b)

{

char item;

pthread_mutex_lock(&b->mutex);

while(b->occupied <= 0)

pthread_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 next occupied 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) */



pthread_cond_signal(&b->less);

pthread_mutex_unlock(&b->mutex);



return(item);

}

你可能感兴趣的:(Linux多线程编程)