pthread_cond_wait

为什么pthread_cond_wait需要互斥锁mutex作为参数

通常的应用场景下,当前线程执行pthread_cond_wait时,一定是处于某个临界区,正在访问共享资源,存在一个mutex与该临界区相关联。因此,在阻塞前,必须释放mutex;被唤醒后,仍然处于临界区,因此需要再次获得mutex。

目录
为什么是pthread_cond_wait(cond, mutex)而不是pthread_cond_wait(cond)
生产者和消费者问题的介绍
用于同步和互斥的全局变量
使用pthread_cond_wait(cond)解决生产者和消费者问题(第一版)
使用pthread_cond_wait(cond)解决生产者和消费者问题(第二版)
使用pthread_cond_wait(cond, mutex)解决生产者和消费者问题
完整可编译运行的程序
程序运行结果
1. 为什么是pthread_cond_wait(cond, mutex)而不是pthread_cond_wait(cond)
我当初学习条件变量时,也有过和楼主相同的疑问,在上操作系统实践课程时,班上的个别学生也问过这个问题。相信这是一个初学者的共性问题,但很少有书籍仔细解释这个问题。
为什么pthread_cond_wait的api被设计为
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
而不是被设计为
int pthread_cond_wait(pthread_cond_t *cond);
pthread_cond_wait(cond, mutex)的功能有3个:

调用者线程首先释放mutex
然后阻塞,等待被别的线程唤醒
当调用者线程被唤醒后,调用者线程会再次获取mutex
pthread_cond_wait(cond)的功能只有1个:
调用者线程阻塞,等待被别的线程唤醒。
这里首先给一个简洁的回答:

通常的应用场景下,当前线程执行pthread_cond_wait时,处于临界区访问共享资源,存在一个mutex与该临界区相关联,这是理解pthread_cond_wait带有mutex参数的关键
当前线程执行pthread_cond_wait前,已经获得了和临界区相关联的mutex;执行pthread_cond_wait会阻塞,但是在进入阻塞状态前,必须释放已经获得的mutex,让其它线程能够进入临界区
当前线程执行pthread_cond_wait后,阻塞等待的条件满足,条件满足时会被唤醒;被唤醒后,仍然处于临界区,因此被唤醒后必须再次获得和临界区相关联的mutex
综上,调用pthread_cond_wait时,线程总是位于某个临界区,该临界区与mutex相关,pthread_cond_wait需要带有一个参数mutex,用于释放和再次获取mutex。

本文的剩下部分将通过一个具体的应用场景来说明,为什么pthread_cond_wait需要一个看似多余的mutex参数。

2. 生产者和消费者问题的介绍
存在一个共享缓冲区,生产者向共享缓冲区写入数据,消费者从共享缓冲区中读取数据。
生产者和消费者存在同步关系:当共享缓冲区为满时,生产者需要等待,等待消费者从共享缓冲区取走数据;当共享缓冲区为空时,消费者需要等待,等待生产者向共享缓冲区中写入数据。
使用环形队列实现共享缓冲区,数据结构如下:
#define CAPACITY 8     // 缓冲区的最大容量
int buffer[CAPACITY];  // 缓冲区数组
int in;                // 缓冲区的写指针
int out;               // 缓冲区的读指针
int size;              // 缓冲区中的数据个数
缓冲区的相关代码如下

void buffer_init()
{
    in = 0;
    out = 0;
    size = 0;
}

// 判断缓冲区是否为空
int buffer_is_empty()
{
    return size == 0; 
}

// 判断缓冲区是否为满
int buffer_is_full()
{
    return size == CAPACITY; 
}

// 向缓冲区中追加一个数据
void buffer_put(int item)
{
    buffer[in] = item;
    in = (in + 1) % CAPACITY;
    size++;
}

// 从缓冲区中取走一个数据
int buffer_get()
{
    int item;

    item = buffer[out];
    out = (out + 1) % CAPACITY;
    size--;

    return item;
}
如果存在多个生产者和多个消费者,变量in、out和size会被它们共享访问,因此生产者和消费者还存在互斥关系:

当某个生产者执行buffer_is_full、buffer_put时,访问了变量in、out和size,只能允许该生产者独占访问这三个变量,禁止其他生产者和消费者访问这些共享变量。
当某个消费者执行buffer_is_empty、buffer_get时,访问了变量in、out和size,只能允许该消费者独占访问这三个变量,禁止其他生产者和消费者访问这些共享变量。
3. 用于同步和互斥的全局变量
总结以上

生产者和消费者中存在有同步关系,需要使用pthread_cond_wait和pthread_cond_signal解决
生产者和消费者中存在有互斥关系,需要使用pthread_mutex_lock和pthread_mutex_unlock解决
程序中需要引入两个全局变量cond和mutex用于同步和互斥

pthread_cond_t cond;
pthread_mutex_t mutex;
4. 使用pthread_cond_wait(cond)解决生产者和消费者问题(第一版)
下面我们尝试使用没有mutex参数的pthread_cond_wait来模拟生产者消费者,假想中没有mutex参数的pthread_cond_wait原型如下:

int pthread_cond_wait(pthread_cond_t *cond);
pthread_cond_wait(cond)的功能非常简单,仅仅阻塞当前线程。在生产者消费者这个应用场景中,很快就能发现pthread_cond_wait(cond)的问题。

使用pthread_cond_wait(cond)解决生产者和消费者问题的代码如下:

// 生产者线程执行的流程
void producer_loop()
{
    int i;

    // 生产CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) {  
        pthread_mutex_lock(&mutex);

        // 当缓冲区为满时,生产者需要等待
        while (buffer_is_full()) {   
            // 当前线程已经持有了mutex,调用pthread_cond_wait阻塞,必然导致死锁
            pthread_cond_wait(&cond);
        }

        // 此时,缓冲区肯定不是满的,向缓冲区写数据
        buffer_put(i);

        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);
    }
}

// 消费者线程执行的流程
void consumer_loop()
{
    int i;

    // 消费CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) {  
        pthread_mutex_lock(&mutex);

        // 当缓冲区为空时,消费者需要等待
        while (buffer_is_empty()) {   
            // 当前线程已经持有了mutex,调用pthread_cond_wait阻塞,必然导致死锁
            pthread_cond_wait(&cond);
        }

        // 此时,缓冲区肯定不是空的,从缓冲区取数据
        int item = buffer_get();

        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);
    }
}
以上程序存在一个会导致死锁的严重错误,以生产者为例:

当前缓冲区已经满了,生产者运行,首先获取mutex
然后检测buffer_is_full为真,生产者无法放入数据
调用pthread_cond_wait,该生产者进入阻塞状态,等待被消费者唤醒
消费者试图获取mutex,由于mutex已经被占用了,消费者将进入阻塞状态
生产者和消费者均进入阻塞状态,系统死锁
5. 使用pthread_cond_wait(cond)解决生产者和消费者问题(第二版)
为了解决死锁的问题,需要对上一节的程序进行如下改进

调用线程调用pthread_cond_wait(cond)前,已经持有了mutex
执行pthread_cond_wait(cond)前,调用pthread_unlock(mutex)释放mutex
执行pthread_cond_wait(cond)后,调用pthread_lock(mutex)再次获得mutex
// 生产者线程执行的流程
void producer_loop()
{
    int i;

    // 生产CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) {  
        pthread_mutex_lock(&mutex);

        // 当缓冲区为满时,生产者需要等待
        while (buffer_is_full()) {   
            pthread_mutex_unlock(&mutex);
            pthread_cond_wait(&cond);
            pthread_mutex_lock(&mutex);
        }

        // 此时,缓冲区肯定不是满的,向缓冲区写数据
        buffer_put(i);

        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);
    }
}

// 消费者线程执行的流程
void consumer_loop()
{
    int i;

    // 消费CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) {  
        pthread_mutex_lock(&mutex);

        // 当缓冲区为空时,消费者需要等待
        while (buffer_is_empty()) {   
            pthread_mutex_unlock(&mutex);
            pthread_cond_wait(&cond);
            pthread_mutex_lock(&mutex);            
        }

        // 此时,缓冲区肯定不是空的,从缓冲区取数据
        int item = buffer_get();

        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);
    }
}
这里解释一下为什么线程调用pthread_cond_wait返回后,需要再次调用pthread_mutex_lock获取锁。以生产者为例,以下为生产者向buffer中追加数据的代码段:

// 生产者向buffer中追加数据
pthread_mutex_lock(&mutex);
while (buffer_is_full()) {   
    pthread_mutex_unlock(&mutex);
    pthread_cond_wait(&cond);
    pthread_mutex_lock(&mutex);
}
buffer_put(i);
pthread_mutex_unlock(&mutex);
在上面这段代码中,生产者线程会调用buffer_is_full和buffer_put,访问共享变量in、out和size。必须保证线程以独占的方式访问这些共享变量,即线程在调用buffer_is_full和buffer_put前必须持有锁。线程从pthread_cond_wait返回后,调用pthread_mutex_lock再次获得锁,然后执行语句while (buffer_is_full())时,因为已经拥有了锁,所以通过buffer_is_full访问共享变量是安全的。

6. 使用pthread_cond_wait(cond, mutex)解决生产者和消费者问题
在上一个版本的程序中,生产者和消费者中存在如下代码段

// 先释放mutex、再阻塞、最后再次获取mutex
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
从这个应用场景来看,pthread_cond_wait被设计为带有mutex参数,用一次函数调用pthread_cond_wait(cond, mtex)即可实现以上三条语句的功能。

使用pthread_cond_wait(cond, mutex)解决生产者和消费者问题的代码如下:

// 生产者线程执行的流程
void producer_loop()
{
    int i;

    // 生产CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) 
    {  
        pthread_mutex_lock(&mutex);

        // 当缓冲区为满时,生产者需要等待
        while (buffer_is_full()) 
        {   
            // 当前线程已经持有了mutex,首先释放mutex,然后阻塞,醒来后再次获取mutex            
            pthread_cond_wait(&cond, &mutex);
        }

        // 此时,缓冲区肯定不是满的,向缓冲区写数据
        buffer_put(i);

        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);
    }
}

// 消费者线程执行的流程
void consumer_loop()
{
    int i;

    // 消费CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) 
    {  
        pthread_mutex_lock(&mutex);

        // 当缓冲区为空时,消费者需要等待
        while (buffer_is_empty()) 
        {  
            // 当前线程已经持有了mutex,首先释放mutex,然后阻塞,醒来后再次获取mutex 
            pthread_cond_wait(&cond, &mutex);
        }

        // 此时,缓冲区肯定不是空的,从缓冲区取数据
        int item = buffer_get();

        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);
    }
}

7. 完整可编译运行的程序
#include <stdio.h>
#include <pthread.h>

#define CAPACITY 8     // 缓冲区的最大容量
int buffer[CAPACITY];  // 缓冲区数组
int in;                // 缓冲区的写指针
int out;               // 缓冲区的读指针
int size;              // 缓冲区中的数据个数

void buffer_init()
{
    in = 0;
    out = 0;
    size = 0;
}

// 判断缓冲区是否为空
int buffer_is_empty()
{
    return size == 0; 
}

// 判断缓冲区是否为满
int buffer_is_full()
{
    return size == CAPACITY; 
}

// 向缓冲区中追加一个数据
void buffer_put(int item)
{
    buffer[in] = item;
    in = (in + 1) % CAPACITY;
    size++;
}

// 从缓冲区中取走一个数据
int buffer_get()
{
    int item;

    item = buffer[out];
    out = (out + 1) % CAPACITY;
    size--;

    return item;
}

pthread_cond_t cond;
pthread_mutex_t mutex;

// 生产者线程执行的流程
void *producer_loop(void *arg)
{
    int i;

    // 生产CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) 
    {       
        printf("produce %d\n", i);
        pthread_mutex_lock(&mutex);

        // 当缓冲区为满时,生产者需要等待
        while (buffer_is_full()) 
        {   
            // 当前线程已经持有了mutex,首先释放mutex,然后阻塞,醒来后再次获取mutex
            pthread_cond_wait(&cond, &mutex);
        }

        // 此时,缓冲区肯定不是满的,向缓冲区写数据
        buffer_put(i);
        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);
    }

    return NULL;
}

// 消费者线程执行的流程
void *consumer_loop(void *arg)
{
    int i;

    // 消费CAPACITY*2个数据
    for (i = 0; i < CAPACITY*2; i++) 
    {  
        pthread_mutex_lock(&mutex);

        // 当缓冲区为空时,消费者需要等待
        while (buffer_is_empty()) 
        {   
            // 当前线程已经持有了mutex,首先释放mutex,然后阻塞,醒来后再次获取mutex
            pthread_cond_wait(&cond, &mutex);
        }

        // 此时,缓冲区肯定不是空的,从缓冲区取数据
        int item = buffer_get();
        pthread_mutex_unlock(&mutex);        

        // 缓冲区的状态发生了变化,唤醒其它的生产者或消费者
        pthread_cond_signal(&cond);

    printf("\tconsume %d\n", item);
    }

    return NULL;
}

int main()
{
    pthread_t producer;
    pthread_t consumer;

    buffer_init();
    pthread_create(&producer, NULL, producer_loop, NULL);
    pthread_create(&consumer, NULL, consumer_loop, NULL);

    pthread_join(producer, NULL);
    pthread_join(consumer, NULL);
    return 0;
}

8. 程序运行结果
root@linuxmooc:~# cc -o thread thread.c -lpthread
root@linuxmooc:~# ./thread
produce 0
produce 1
produce 2
produce 3
produce 4
produce 5
produce 6
produce 7
produce 8
produce 9
        consume 0
        consume 1
        consume 2
        consume 3
        consume 4
        consume 5
        consume 6
        consume 7
        consume 8
produce 10
produce 11
produce 12
produce 13
produce 14
produce 15
        consume 9
        consume 10
        consume 11
        consume 12
        consume 13
        consume 14
        consume 15

由于工作上的事情,要用到线程之间的同步,而且有超时处理,在网上看到了使用pthread_cond_timedwait()函数和pthread_cond_wait()函数,其实2个函数都差不多,我主要是要用pthread_cond_timedwait()函数。

pthread_cond_timedwait()函数有三个入口参数:
(1)pthread_cond_t __cond:条件变量(触发条件)
(2)pthread_mutex_t __mutex: 互斥锁
(3)struct timespec __abstime: 等待时间(其值为系统时间 + 等待时间)
当在指定时间内有信号传过来时,pthread_cond_timedwait()返回0,否则返回一个非0数(我没有找到返回值的定义);

在使用pthread_cond_timedwait()函数时,必须有三步:
1:加互斥锁:pthread_mutex_lock(&__mutex)
2:等待:pthread_cond_timedwait(&__cond, &__mutex, &__abstime)   //解锁->等待->加锁
3:解互斥锁:pthread_mutex_unlock(&__mutex)

发送信号量时,也要有三步:

1:加互斥锁:pthread_mutex_lock(&__mutex)
2:发送:pthread_cond_signal(&__cond)
3:解互斥锁:pthread_mutex_unlock(&__mutex)

那么,这里就有一个问题,等待的时候已经加上锁了,那么我发送的时候怎么才能运行到发送函数呢?其实这是因为在pthread_cond_timedwait()函数中已经对互斥锁进行解锁操作了,所以这个时候发送信号量是不会阻塞的。其实仔细想想,这样不是才能保证同步吗?(写完代码后考虑一下)

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/time.h>
 
#define SENDSIGTIME 10
 
pthread_cond_t g_cond;
pthread_mutex_t g_mutex;
 
void thread1(void *arg)
{
    int inArg = (int)arg;
    int ret = 0;
    struct timeval now;
    struct timespec outtime;
 
    pthread_mutex_lock(&g_mutex);
 
    gettimeofday(&now, NULL);
    outtime.tv_sec = now.tv_sec + 5;
    outtime.tv_nsec = now.tv_usec * 1000;
 
    ret = pthread_cond_timedwait(&g_cond, &g_mutex, &outtime);
    //ret = pthread_cond_wait(&g_cond, &g_mutex);
    pthread_mutex_unlock(&g_mutex);
 
    printf("thread 1 ret: %d\n", ret);
}
 
int main(void)
{
    pthread_t id1;
    int ret;
 
    pthread_cond_init(&g_cond, NULL);
    pthread_mutex_init(&g_mutex, NULL);
 
    ret = pthread_create(&id1, NULL, (void *)thread1, (void *)1);
    if (0 != ret)
    {
    printf("thread 1 create failed!\n");
    return 1;
    }
 
    printf("等待%ds发送信号!\n", SENDSIGTIME);
    sleep(SENDSIGTIME);
    printf("正在发送信号....\n");
    pthread_mutex_lock(&g_mutex);
    pthread_cond_signal(&g_cond);
    pthread_mutex_unlock(&g_mutex);
 
 
    pthread_join(id1, NULL);
 
    pthread_cond_destroy(&g_cond);
    pthread_mutex_destroy(&g_mutex);
 
    return 0;
}


 

由于工作上的事情,要用到线程之间的同步,而且有超时处理,在网上看到了使用pthread_cond_timedwait()函数和pthread_cond_wait()函数,其实2个函数都差不多,我主要是要用pthread_cond_timedwait()函数。

pthread_cond_timedwait()函数有三个入口参数:
(1)pthread_cond_t __cond:条件变量(触发条件)
(2)pthread_mutex_t __mutex: 互斥锁
(3)struct timespec __abstime: 等待时间(其值为系统时间 + 等待时间)
当在指定时间内有信号传过来时,pthread_cond_timedwait()返回0,否则返回一个非0数(我没有找到返回值的定义);

在使用pthread_cond_timedwait()函数时,必须有三步:
1:加互斥锁:pthread_mutex_lock(&__mutex)
2:等待:pthread_cond_timedwait(&__cond, &__mutex, &__abstime)   //解锁->等待->加锁
3:解互斥锁:pthread_mutex_unlock(&__mutex)

发送信号量时,也要有三步:

1:加互斥锁:pthread_mutex_lock(&__mutex)
2:发送:pthread_cond_signal(&__cond)
3:解互斥锁:pthread_mutex_unlock(&__mutex)

那么,这里就有一个问题,等待的时候已经加上锁了,那么我发送的时候怎么才能运行到发送函数呢?其实这是因为在pthread_cond_timedwait()函数中已经对互斥锁进行解锁操作了,所以这个时候发送信号量是不会阻塞的。其实仔细想想,这样不是才能保证同步吗?(写完代码后考虑一下)

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/time.h>
 
#define SENDSIGTIME 10
 
pthread_cond_t g_cond;
pthread_mutex_t g_mutex;
 
void thread1(void *arg)
{
    int inArg = (int)arg;
    int ret = 0;
    struct timeval now;
    struct timespec outtime;
 
    pthread_mutex_lock(&g_mutex);
 
    gettimeofday(&now, NULL);
    outtime.tv_sec = now.tv_sec + 5;
    outtime.tv_nsec = now.tv_usec * 1000;
 
    ret = pthread_cond_timedwait(&g_cond, &g_mutex, &outtime);
    //ret = pthread_cond_wait(&g_cond, &g_mutex);
    pthread_mutex_unlock(&g_mutex);
 
    printf("thread 1 ret: %d\n", ret);
}
 
int main(void)
{
    pthread_t id1;
    int ret;
 
    pthread_cond_init(&g_cond, NULL);
    pthread_mutex_init(&g_mutex, NULL);
 
    ret = pthread_create(&id1, NULL, (void *)thread1, (void *)1);
    if (0 != ret)
    {
    printf("thread 1 create failed!\n");
    return 1;
    }
 
    printf("等待%ds发送信号!\n", SENDSIGTIME);
    sleep(SENDSIGTIME);
    printf("正在发送信号....\n");
    pthread_mutex_lock(&g_mutex);
    pthread_cond_signal(&g_cond);
    pthread_mutex_unlock(&g_mutex);
 
 
    pthread_join(id1, NULL);
 
    pthread_cond_destroy(&g_cond);
    pthread_mutex_destroy(&g_mutex);
 
    return 0;
}

  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值