我们在开发的过程中,经常遇到多个线程访问同一资源的情况,也就是所谓的临界资源。同一个资源对多个线程同时可见,如果均只是读访问,那没毛病,关键是实际生产中读写是一起的。那么问题来了,一个线程在写数据,另外一个线程在读数据,那么读线程读取的很可能是乱码,即使运气好不是乱码,也不是想要的数据,读取的都是脏数据(这里盗用一下数据库术语)。更糟糕的是,多个线程同时在写数据,你想象一下,那将是多么混乱,数据将是一锅粥。说到这里,那么线程间同步的方式有哪些呢?互斥量(Mutex)、条件变量(Condition)、时间(Event)、临界区(CriticalSection)、信号量(Semphore)等等,这里博主只是列举出来一些常见的方法。
本篇博文我们将一起探讨条件变量(condition),condition统称和mutex一起使用,避免多个线程同时访问condition,进而出现混乱。首先,我们一起来认识几个常用的API吧。
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_destroy(pthread_cond_t *cond);
条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态方式调用pthread_cond_init()函数尽管POSIX标准中为条件变量定义了属性,但在LinuxThreads中没有实现,因此cond_attr值通常为NULL,且被忽略。注销一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能注销这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const struct timespec *abstime);
pthread_cond_wait总和一个互斥锁结合使用。在调用pthread_cond_wait前要先获取锁。pthread_cond_wait函数执行时先自动释放指定的锁(让其他线程可以获得锁,进而可以是袭击获得条件变量,避免死锁),然后等待条件变量的变化(由pthread_cond_signal和pthread_cond_broadcast函数唤醒,即获得条件变量)。在函数调用返回之前,自动将指定的互斥量重新锁住(在没有获得锁之前函数阻塞)。pthread_cond_timedwait函数和pthread_cond_wait类似,pthread_cond_wait在获得条件变量之前会一直阻塞在那里,即永久等待。然而,pthread_cond_timedwait则灵活一点,允许设置等待时间,等待时间过后还没有获得条件变量,函数将不再等待。当然,最后的mutex还是需要等待的,获得mutex后函数立即返回,返回错误码ETIMEDOUT。
intpthread_cond_signal(pthread_cond_t * cond);
pthread_cond_broadcast(pthread_cond_t* cond);
pthread_cond_signal通过条件变量cond发送消息,若多个消息在等待,它只唤醒一个。pthread_cond_broadcast 可以唤醒所有。调用pthread_cond_signal后要立刻释放互斥锁,因为pthread_cond_wait的最后一步是要将指定的互斥量重新锁住,如果pthread_cond_signal之后没有释放互斥锁,pthread_cond_wait仍然要阻塞。
互斥量(也称为互斥锁)出自POSIX线程标准,可以用来同步同一进程中的各个线程。当然如果一个互斥量存放在多个进程共享的某个内存区中,那么还可以通过互斥量来进行进程间的同步。互斥量,从字面上就可以知道是相互排斥的意思,它是最基本的同步工具,用于保护临界区(共享资源),以保证在任何时刻只有一个线程能够访问共享的资源。
int pthread_mutex_destroy(pthread_mutex_t*mutex);
int pthread_mutex_init(pthread_mutex_t*restrict mutex,const pthread_mutexattr_t *restrict attr);
上面两个函数分别由于互斥量的初始化和销毁。如果互斥量是静态分配的,可以通过常量进行初始化:
pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
当然也可以通过pthread_mutex_init()进行初始化。对于动态分配的互斥量由于不能直接赋值进行初始化就只能采用这种方式进行初始化,pthread_mutex_init()的第二个参数是互斥量的属性,如果采用默认的属性设置,可以传入NULL。当不在需要使用互斥量时,需要调用pthread_mutex_destroy()销毁互斥量所占用的资源。
由于本篇博文主要将条件变量(condition的使用),关于mutex的更详尽学习,博主后期会单独写一篇博文,还要牵扯到mutex的属性设置相关函数。关于mutex的函数就介绍到这里,下面我们看个程序实例吧。相信大家在学习操作系统的时候,都有接触到生产者消费者模型、邮箱模型吧。博主这里写了一个测试程序,就是典型的生产者消费者模型。好了,不多说了,直接上代码吧。
程序实例
/*
*
*使用pthread_cond_t和pthread_mutex_t实现一个多线程下的生产者消费者模型
*
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <queue>
#include <sys/time.h> //
#include <signal.h>
#include <errno.h>
#define PROCEDURE 50//生成线程数
#define CONSUMER 30//消费线程数
//打印指定错误号出错信息
#define handle_error_no(no, msg)\
do {errno = no; perror(msg); exit(EXIT_FAILURE);}while(0)
//打印错误信息
#define handle_error(msg)\
do {perror(msg); exit(EXIT_FAILURE);}while(0);
//释放mutex锁,并退出线程
//将退出信息通过pthread_exit函数传递给主线程(主线程使用pthread_join的第二个参数接收)
#define thread_exit_id(msg, id, mutex)\
do {char *buf=(char*)malloc(256);sprintf(buf, "%d号%s退出\n",id, msg);pthread_mutex_unlock(&mutex);pthread_exit(buf);}while(0)
//退出线程
#define thread_pro_exit(msg, id)\
do {char *buf=(char*)malloc(256);sprintf(buf, "%d号%s退出\n",id, msg);pthread_exit(buf);}while(0)
pthread_cond_t cond; //定义条件变量
pthread_mutex_t mutex; //定义mutex(使访问产品队列和修改产品编号操作时原子的)
pthread_mutex_t mutex_shift;//控制开关锁(此锁在同一时间只有两个线程在争用,因为此锁只存在于生产线程的mutex锁内)
bool shift = true; //生产按钮,初始状态为开始状态
//产品结构体
typedef struct produce
{
int pro_num;//生产产品号
}produce_t;
std::queue<produce_t> pro_queue; //已生产产品队列
int pro_num = 0; //生产的产品号(从1开始编号)
//中断信号(【ctrl+c】)处理函数
//使生产线程结束生产
void inthandler(int signum)
{
//加锁操作生产开关
pthread_mutex_lock(&mutex_shift);
shift = false;//将生产开关关闭
//fprintf(stdout, "stop now!\n");
pthread_mutex_unlock(&mutex_shift);
}
//生产线程回调函数
void * procedure(void *arg)
{
int id = *(int *)arg;//接收线程参数
free(arg);//释放参数资源(在主线程中申请的堆上空间)
produce_t produces;//存放待生产产品
while (1)
{
pthread_mutex_lock(&mutex);//加锁
//访问生产开关前先加锁
//当用户发出中断信号,此时只有信号处理函数和当前线程在争用此锁
pthread_mutex_lock(&mutex_shift);
if (!shift)
{
pthread_mutex_unlock(&mutex_shift);//退出前先释放锁
break;
}
pthread_mutex_unlock(&mutex_shift);
++pro_num;//产品号加1
fprintf(stdout, "%d生产线程开始生产第%d号产品\n", id, pro_num);
usleep(10);//等待10微妙
produces.pro_num = pro_num;
fprintf(stdout, "%d生产线程结束生产第%d号产品\n", id, pro_num);
pro_queue.push(produces);//将生产的产品加入已生产产品队列
//pthread_cond_signal(&cond);//随机唤醒一个等待线程
pthread_cond_broadcast(&cond);//唤醒所用等待线程
pthread_mutex_unlock(&mutex);//解锁
sched_yield();//让出CPU资源,防止此线程一直占用资源
}
//退出线程,在退出前先释放mutex锁资源
thread_exit_id("生产线程", id, mutex);
}
//消费线程回调函数
void * consumer(void *arg)
{
int id = *(int *)arg;//接收线程参数
free(arg);//是否参数资源(在主线程中申请的堆上空间)
produce_t produces;//存放贷消费产品
struct timespec outtime;//设置超时时间用
struct timeval nowtime;//存放当前系统时间用
while (1)
{
pthread_mutex_lock(&mutex);//加锁
//当没有产品时,等待
if (pro_queue.empty())
{
fprintf(stdout, "%d号消费线程等待消费产品开始\n", id);
//等待生产线程的条件
//调用该函数是,首先释放锁,在函数调用结束时加锁,使恢复到加锁状态
//pthread_cond_wait(&cond, &mutex);
gettimeofday(&nowtime, NULL);
outtime.tv_sec = nowtime.tv_sec + 1;
outtime.tv_nsec = nowtime.tv_usec * 1000;
int ret = pthread_cond_timedwait(&cond, &mutex, &outtime);
if (ETIMEDOUT == ret) thread_exit_id("消费线程",id, mutex);
fprintf(stdout, "%d号消费线程等待消费产品结束\n", id);
}
produces = pro_queue.front();//从已生产产品队列中取出队头产品
fprintf(stdout, "%d号消费线程消费产品%d开始\n", id, produces.pro_num);
usleep(10);//休眠10微妙
fprintf(stdout, "%d号消费线程消费产品%d结束\n", id, produces.pro_num);
pro_queue.pop();//从队列中删除该产品
pthread_mutex_unlock(&mutex);//释放锁
sched_yield();//让出CPU资源
}
}
int main()
{
pthread_t tid[PROCEDURE+CONSUMER];//定义线程id数组
int i = 0;//循环变量
pthread_cond_init(&cond, NULL);//初始化条件量
pthread_mutex_init(&mutex, NULL);//初始化mutex
pthread_mutex_init(&mutex_shift, NULL);//初始化mutex
//注册中断信号
if (SIG_ERR == signal(SIGINT, inthandler))handle_error("signal");
//创建生产线程
for (i = 0; i < PROCEDURE; ++i)
{
int *arg = (int *)malloc(sizeof(int));//为线程参数申请堆上空间
if (NULL == arg) handle_error("malloc");
*arg = i+1;//记录线程号
int ret = pthread_create(tid+i, NULL, procedure, arg);
if (0 != ret) handle_error_no(ret, "pthread_creat");
}
//创建消费线程
for (; i < PROCEDURE+CONSUMER; ++i)
{
int *arg = (int *)malloc(sizeof(int));//为线程参数申请堆上空间
if (NULL == arg) handle_error("malloc");
*arg = i - PROCEDURE + 1;//记录线程号
int ret = pthread_create(tid + i, NULL, consumer, arg);
if (0 != ret) handle_error_no(ret, "pthread_creat");
}
void *buf = NULL;
//主线程阻塞等待子线程退出
for (i = 0; i < PROCEDURE + CONSUMER; ++i)
{
//等待子线程退出,接收pthread_exit传出的参数
pthread_join(tid[i], &buf);
fprintf(stdout, "%s", (char*)buf);
free((char*)buf);//释放堆上空间
buf = NULL;
}
pthread_cond_destroy(&cond);//销毁条件变量
pthread_mutex_destroy(&mutex);//销毁mutex
pthread_mutex_destroy(&mutex_shift);//销毁mutex
}
程序截图:
程序运行的比较快,这里博主只截取了部分运行结果。在上述程序中,有用到前面讲到过得信号,作为最后退出的切入点。博主在程序中添加了很详尽的注释,相信大家已经看明白了,恭喜你又获得了一项新技能。由于实例比较简单,注释也是相当的详尽,博主就不再对程序进行赘述了,小伙伴们,赶紧狂敲代码测试吧。