从微观上来讲,线程同步是保证线程互斥的同时,还保证了线程的执行顺序。但就我们目前所学,无论是互斥锁还是读写锁,虽然都是线程同步的一种方式,但是它们都有自身的一些局限性,例如:只能保证线程互斥,无法保证线程的执行顺序。
条件变量也是pthread线程库提供的一种同步机制,通常与互斥锁配合使用,需要注意的是条件变量本身不是锁,而是给多线程提供一个会合的场所。
为什么条件变量要配合互斥锁使用?
互斥量只能保证线程访问共享数据不出错,而条件变量可以协调线程的执行顺序,从而保证线程 安全,有顺序
的访问共享数据。
再强调一遍,条件变量并不是锁,但是可以通过调用条件变量相关API函数来造成线程阻塞,唤醒等,通常配合互斥量使用,很多操作并不是直接针对互斥量的,而是通过条件变量来影响互斥量。
条件变量允许线程阻塞等待特定条件发生,当条件不满足时,则调用条件变量的等待相关函数,让线程进入阻塞状态并等待条件发生改变,一旦某个线程满足条件就调用条件变量的通知相关函数,可通知唤醒一个或多个阻塞的线程。
那么条件变量为什么会阻塞线程?其实条件变量本质上是一个等待队列,会把所有阻塞等待的线程放置在等待队列,而互斥量用来保护等待队列,所以条件变量通常和互斥锁一起使用。
1. 首先生产者负责生产,消费者负责消费。如果生产者线程没有生产出产品(条件不成立)
,此时消费者线程应该调用相应的wait函数加入到线程等待队列中(条件变量)阻塞,等待被唤醒。
2. 当生产者线程生产出产品了(条件成立)
,生产者线程需要调用通知相关函数唤醒正在等待的消费者线程去消费。
也就是说生产者线程调用pthread_cond_signal函数和pthread_cond_broadcast函数唤醒等待队列中一个或多个消费者线程的同时,还会把消费者线程从等待队列中踢出,因为等待队列是用于放置阻塞的线程,而被唤醒的消费者线程由于不再阻塞,也就没必要在等待队列中,此时该线程会从等待队列中删除。
3. 如果等待队列中有多个阻塞的消费者线程,那么可能会同时唤醒多个消费者线程同时去消费产品,为了避免这种情况,需要互斥锁对等待队列上锁进行保护,从而达到保护线程对共享数据的访问。
//初始化一个条件变量
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
//销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//阻塞等待一个条件变量是否满足条件
//参数cond指定条件变量,参数mutex指定互斥量
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//限时阻塞等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
//唤醒至少一个阻塞在条件变量上的线程,也有可能会唤醒多个线程
int pthread_cond_signal(pthread_cond_t *cond);
//唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
以上6 个函数的返回值都是:成功返回0, 失败直接返回错误,pthread_cond_t是用于定义条件变量的数据类型,pthread_condattr_t 用于定义条件变量属性。
一个线程去负责计算结果,一个线程去负责获取结果
#include
#include
#include
#include
#include
typedef struct{
int res; //用于存放结果
int is_wait; //用户给出的判断条件 ,表示线程的状态,0即未创建,1即已创建
pthread_cond_t cond; //定义条件变量
pthread_mutex_t mutex; //定义互斥锁
} Result;
//计算结果的线程主控函数
void * set_fn(void *arg){
int i , sum = 0;
for(; i <= 100; ++i){
sum += i;
}
Result *r = (Result *)arg;
//把结果放入result中的res后,接下来就是判断获取结果的线程的状态
r->res = sum;
//is_wait是共享数据,要加锁
pthread_mutex_lock(&r->mutex);
//根据is_wait来判断获取结果线程状态是否准备好
//0表示还没还没创建,1表示线程已创建,并已经加入等待队列
while(!r->is_wait){
//因为不释放锁的话,获取结果线程就无法设置判断条件为1了,会造成死锁现象
pthread_mutex_unlock(&r->mutex);
//休眠的目的是为了让获取结果线程在此休眠期间获得执行,以此来设置判断条件,加入等待队列等操作
usleep(200);
//这里加锁的目的是可能还要继续循环判断,所以这里必须再次加锁
pthread_mutex_lock(&r->mutex);
}
//如果跳出循环,说明获取结果线程已经创建好,正在处理阻塞状态,所以必须释放锁
pthread_mutex_unlock(&r->mutex);
//然后通知(唤醒)等待的获取结果线程
pthread_cond_broadcast(&r->cond);
return (void *)0;
}
//获取结果的线程主控函数
void *get_fn(void *arg){
Result *r = (Result *)arg;
//is_wait是共享数据,操作前加锁
pthread_mutex_lock(&r->mutex);
r->is_wait = 1;
//创建好后就把该线程加入到等待队列中进行等待,然后等待计算结果线程通知(唤醒)此线程
//这里有一个问题:为什么wait函数要放在unlock函数前面呢?
pthread_cond_wait(&r->cond , &r->mutex);
pthread_mutex_unlock(&r->mutex);
//被计算结果线程通知(唤醒)后,然后去获取计算结果
//如果获取计算结果线程还没有执行完,又被计算结果线程调度执行,数据就会出现问题
pthread_mutex_lock(&r->mutex);
int res = r->res;
printf("tid = %lu is get res = %d\n", pthread_self() , res);
pthread_mutex_unlock(&r->mutex);
return (void *)0;
}
int main(void){
int ret;
pthread_t cal , get;
Result r;
//初始化判断条件
r.is_wait = 0;
//初始化条件变量和互斥锁
pthread_cond_init(&r.cond , NULL);
pthread_mutex_init(&r.mutex , NULL);
//创建计算结果的线程和获取结果线程
ret = pthread_create(&cal , NULL , set_fn , (void *)&r);
if(ret != 0){
perror("pthread_create cal");
return -1;
}
ret = pthread_create(&get , NULL , get_fn , (void *)&r);
if(ret != 0){
perror("pthread_create get");
return -1;
}
//阻塞回收线程
pthread_join(cal , NULL);
pthread_join(get , NULL);
//销毁互斥锁,条件变量
pthread_mutex_destroy(&r.mutex);
pthread_cond_destroy(&r.cond);
return 0;
}
在这一小节中,我们遗留了一个问题:
- 为什么wait函数要放在unlock函数前面
我们简单看一下pthread_cond_wait函数的内部实现:
pthread_cond_wait() 函数的内部实现机制{
1. unlock(&mutex); /* 释放锁 */
2. lock(&mutex);
3. 将该线程加入到等待队列中
4. unlock(&mutex);
5. 当前线程阻塞,然后其他线程去通知(唤醒)该线程
6. 唤醒后,然后调用lock
7. 从等待队列中删除该线程
}
第1 ,2 ,3 ,4步:
wait函数内部需要去操作等待队列,把线程加入到等待队列中,而等待队列是一个共享的数据,有可能其他线程也要加入到等待队列中,为了保证线程安全,必须要对等待队列进行保护上锁。不仅如此,wait函数内部第一次解锁是针对is_wait条件(is_wait也是共享数据)的保护而解锁(因为你之前已经上锁了)。
第5步:
然后当前线程阻塞,等待其他线程通过调用pthread_cond_signal函数将改该线程唤醒。
第6 , 7步:
一旦等待队列中的线程被唤醒,会再次进行加锁把唤醒的线程从等待队列中删除掉,加锁的目的是为了保证删除操作不可被其他线程打断(等待队列是个共享数据,操作时需要加锁),所以这就是为什么调用完pthread_cond_wait函数后,紧接着要调用pthread_cond_unlock进行解锁的原因了。
这也间接说明了为什么条件变量和互斥量一起配合使用了。
相较于mutex而言,条件变量可以减少竞争。
如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果临界区中没有数据,消费者之间竞争互斥锁是无意义的。加入条件变量机制以后,只有当生产者完成生产,才会引起消费者之间的竞争,从而提高了程序效率。