上一节提到了线程互斥和同步的概念,并且给出了两种用于解决共享资源互斥的利器:互斥锁和读写锁。那么本节将介绍两种用于解决线程同步的概念:条件变量和信号量。
一. 条件变量
1.基本概念
互斥锁的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足。条件变量的内部实质上是一个等待队列,放置等待(阻塞)的线程,线程在条件变量上等待和通知,互斥锁用来保护等待队列(因为所有的线程都可以放入等待队列,所以等待队列成为了一个共享的资源,需要被上锁保护),因此条件变量通常和互斥锁一起使用。
条件变量允许线程等待特定条件(判断条件一般由用户自己给出)发生,当条件不满足时,线程通常先进入阻塞状态,等待条件发生变化。一旦其他的某个线程改变了条件,就可以唤醒等待队列中的一个或多个阻塞的线程。
2.相关定义
数据类型:pthread_cond_t;
初始化:pthread_cond_init();
销毁:pthread_cond_destroy();
条件变量等待操作:pthread_cond_wait();
带有超时的等待:pthread_cond_timewait();
条件变量的解除等待操作:pthread_cond_signal();——通知单个线程
条件变量的解除等待操作:pthread_cond_broadcast();——通知所有线程
下面给出一个简单的案例来说明条件变量的用法与作用。
该案例主要实现:一个线程用于计算某一结果,另一个线程用于获取该计算结果,当计算线程完成计算前,获取结果的线程需要处于等待的状态。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
/*
* 完成一个线程负责计算结果,一个线程负责获取结果
* 用到条件变量
*/
typedef struct
{
int res; //存储计算的结果
int is_wait; // 用户给出用于判断的条件
pthread_cond_t cond; // 条件变量
pthread_mutex_t mutex; // 互斥锁(保护等待队列)
}Result;//共享资源
// 计算并将结果放置于Result中的线程运行函数
void *set_fn(void *arg)
{
int i = 1, sum = 0;
for(; i <= 100; ++i)
sum += i;
Result *r = (Result*)arg;
//将计算结果放置到Result的res中
r->res = sum;
//对两个线程共享的判断条件进行保护
pthread_mutex_lock(&r->mutex);
//判断获取结果的线程是否准备好
while(!r->is_wait)//还未准备好
{
//is_wait是共享资源,需要互斥锁进行保护
//如果这里不释放互斥锁,那么在睡眠期间,
//get_fn线程是无法对is_wait进行操作的
pthread_mutex_unlock(&r->mutex);
usleep(100);
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;
//对两个线程共享的判断条件进行保护(加锁)
//两个线程对判断条件操作是互斥的
pthread_mutex_lock(&r->mutex);
//代表获取结果的线程已经准备好
r->is_wait = 1;
//准备好开始等待(等待被唤醒)
pthread_cond_wait(&r->cond, &r->mutex);
//线程被唤醒后释放互斥锁
//这里的unlock放在wait函数之后
//是因为wait内部最后执行了lock
pthread_mutex_unlock(&r->mutex);
//去获取计算的结果
int res = r->res;
printf("0x%lx get sum is %d\n",
pthread_self(), res);
return (void*)0;
}
int main(void)
{
int err;
pthread_t cal, get;
Result r;
r.is_wait = 0;//用户给出
pthread_cond_init(&r.cond, NULL);
pthread_mutex_init(&r.mutex, NULL);
//启动计算结果的线程
if((err = pthread_create(&get, NULL,
get_fn, (void*)&r)) != 0)
{
perror("pthread create error");
}
//启动获取结果的线程
if((err = pthread_create(&cal, NULL,
set_fn, (void*)&r)) != 0)
{
perror("pthread create error");
}
// 主线程阻塞一下
pthread_join(cal, NULL);
pthread_join(get, NULL);
pthread_cond_destroy(&r.cond);
pthread_mutex_destroy(&r.mutex);
return 0;
}
说明:在get_fn函数中,解锁的语句pthread_mutex_unlock()要放在等待语句pthread_cond_wait()之后,原因是在wait函数内部实现中是先对之前的锁进行解锁,然后将自己放入等待对列中,然后再加锁解锁。
二. 信号量
注:在陈硕的《Linux多线程服务器端编程》这本书中,作者对于信号量表达了这样的观点:我没有遇到过需要使用信号量的情况,无从谈及个人经验。我认为信号量不是必备的同步原语,因为条件变量配合互斥器可以完全替代其功能,而且更不易用错。另外,信号量的另一个问题在于它有自己的技术支持,而通常我们自己的数据结构也有长度值,这就造成了同样的信息存了两份,需要保持一致,这增加了程序员的负担和出错的可能。
是的,确实像作者所说,条件变量和互斥器的配合使用可以解决信号量所能解决的问题,但是我们可能不像作者那样有着丰富的阅历,这里仅将信号量作为一种解决问题的办法讲出来。
1.基本概念
信号量本质上是一个非负整数计数器,代表共享资源的数目,通常是用来控制对共享资源的访问。
信号量可以实现线程的同步和互斥。
通常sem_post()和sem_wait()函数对信号量进行加减操作从而解决线程的同步和互斥。
2. 相关定义
数据类型:sem_t
信号量的创建:sem_init()
信号量的销毁:sem_destroy(sem_t* sem, int pshared, unsigned value);
第一个参数是信号量指针,第二个参数表示是否在进程间共享的标志,第三个参数表示信号量的初值;
信号量的加操作:sem_post();(调用一次加1)
信号量的减操作:sem_wait();阻塞版本(调用一次减1)
信号量的减操作:sem_trywait();非阻塞版本
当线程调用sem_wait()后,若信号量的值小于0则线程阻塞。只有其他线程在调用sem_post对信号量作加操作后并且其值大于等于0时,阻塞的线程才能继续运行。
下面的案例是对上面计算获取结果这个案例的改进,即利用信号量来实现一个进程计算结果,另一个进程获取结果。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
/*
* 完成一个线程负责计算结果,一个线程负责获取结果
*/
typedef struct
{
int res; //存储计算的结果
sem_t sem; // 信号量
}Result;//共享资源
// 计算并将结果放置于Result中的线程运行函数
void *set_fn(void *arg)
{
int i = 1, sum = 0;
for(; i <= 100; ++i)
sum += i;
Result *r = (Result*)arg;
//将计算结果放置到Result的res中
r->res = sum;
//V(1)操作
sem_post(&r->sem);
return (void*)0;
}
// 获取结果的线程运行函数
void *get_fn(void *arg)
{
Result *r = (Result*)arg;
//P(1)操作
sem_wait(&r->sem);
//当set_fn执行了sem_post(),这里就可以获取结果了
//去获取计算的结果
int res = r->res;
printf("0x%lx get sum is %d\n",
pthread_self(), res);
return (void*)0;
}
int main(void)
{
int err;
pthread_t cal, get;
Result r;
//初始化信号量,初始值为1
sem_init(&r.sem, 0, 0);
//启动计算结果的线程
if((err = pthread_create(&cal, NULL,
set_fn, (void*)&r)) != 0)
{
perror("pthread create error");
}
//启动获取结果的线程
if((err = pthread_create(&get, NULL,
get_fn, (void*)&r)) != 0)
{
perror("pthread create error");
}
// 主线程阻塞一下
pthread_join(cal, NULL);
pthread_join(get, NULL);
sem_destroy(&r.sem);
return 0;
}
可以看出,对于本案例,相较于条件变量,使用信号量更加的简单直接。