我们学习了操作系统,想必对生产消费者问题都不陌生。作为同步互斥问题的一个经典案例,生产消费者模型其实是解决实际问题的基础模型,解决很多的实际问题都会依赖于它。而此模型要解决最大的问题便是同步与互斥。而通常呢,在多进程的环境下我们一般是是用信号量来解决(可以戳这里看看);在多线程的情况,则会用到两个东西: 互斥量和条件变量。通常用它们两个来实现线程间通信,以此来解决多线程下的同步和互斥问题。不过在具体实现生产消费模型前,为了更好理解当中的处理原理,还是先来回顾一下一些线程间通信的相关知识。
互斥问题
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
// 操作共享变量会有问题的售票系统代码 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> int ticket = 100; void *route( void *arg) { char id = *(char*)arg; while ( 1 ) { if ( ticket > 0 ) { usleep( 1000) ; printf( " thread %c sells ticket:%d " , id, ticket) ; ticket--; } else { break; } } } int main(void) { pthread_t t1, t2, t3, t4; char a1=1,a2=2,a3=3,a4=4; pthread_create( &t1, NULL, route, &a1); pthread_create( &t2, NULL, route, &a2); pthread_create( &t3, NULL, route, &a3); pthread_create( &t4, NULL, route, &a4); pthread_join( t1, NULL) ; pthread_join( t2, NULL) ; pthread_join( t3, NULL) ; pthread_join( t4, NULL) ; }
//一次执行结果: thread 4 sells ticket:100 ... thread 4 sells ticket:1 thread 2 sells ticket:0 thread 1 sells ticket:-1 thread 3 sells ticket:-2
为什么无法获得正确结果?
·if 语句判断条件为真以后,代码可以并发的切换到其他线程;usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
·ticket--操作本身就不是一个原子操作
如果取出 “ticket--”部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34
<ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34
ticket--操作并不是原⼦子操作,而是对应三条汇编指令:
load:将共享变量ticket从内存加载到寄存器中
update: 更新寄存器里面的值,执行-1操作
store:将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
1.代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
Linux下用互斥量就做到了以上3点,它本质上其实就是一把锁。
互斥量
互斥量使用一般是以下几个步骤:
1.定义互斥量(mutex): pthread_mutex_t mutex;
2.初始化:
①静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
②动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:如果不设置线程属性的话填NULL
3.上锁:pthread_mutex_lock(&mutex) 如果是1,值0,返回; 如果是0,便阻塞
调⽤用pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,
但没有竞争到互斥量,那么pthread_ lock调⽤用会陷入阻塞,等待互斥量解锁。
4.解锁: pthread_mutex_unlock(&mutex) 置为1,返回
5.销毁:pthread_mutex_destroy(&mutex)
销毁互斥量需要注意:使⽤用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
不要销毁⼀一个已经加锁的互斥量已经销毁的互斥量,要确保后⾯面不会有线程再尝试加锁
自旋锁
互斥锁是当阻塞在pthread_mutex_lock时,放弃CPU,好让别人使用CPU。自旋锁阻塞在spin_lock时不会阻塞CPU,不断对CPU询问。(实时系统中应用比较多,要求对锁进行较快响应)它使用形式与互斥量类似,不再赘述。
1.定义自旋锁: pthread_spinlock_t spin
2.初始化自旋锁:pthread_spin_intt(pthread_spinlock_t *s, int s)
3.上锁:int pthread_spin_lock(pthread_spinlock_t *lock)
4.解锁:int pthread_spin_lock(pthread_spinlock_t *lock)
5.销毁:int pthread_spin_lock(pthread_spinlock_t *lock)
读写锁
在编写多线程的时候,有⼀一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极⼤大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。读写锁本质上是一种自旋锁[长时间等人和短时间等人的例子]
·注意:读共享,写排他,写优先级高
它处理方式和前面互斥量类似,就不在赘述。
1.定义:pthread_rwlock_t lock
2.初始化 pthread_rwlock_init(&lock, NULL)
3.上锁:pthread_rwlock_rdlock(&lock) pthread_rwlock_wrlock(&lock)
4.解锁:pthread_rwlock_unlock(&lock)
5.销毁:pthread_rwlock_destroy(&lock)
条件变量
生产者消费问题要解决另一个问题就同步的问题。当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况下就需要用到条件变量了。
使用步骤如下:
1.定义条件变量:
pthread_cond_t cond;
pthread_mutex_t mutex
2.初始化条件变量:
pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *restrict attr) 参数: cond:要初始化的条件变量 attr:填NULL(用于设置线程属性)
3.等待条件:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后⾯面详细解释
注意这里wait函数需要互斥量(后面解释)。如果在锁环境下,此处互斥量形同虚设。在锁环境下,会将mutex解锁; wait返回时,将mutex锁制成原来状态
4.使条件满足 :
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
5.销毁条件变量:
int pthread_cond_destroy(pthread_cond_t *cond)
为什么pthread_ cond_ wait 需要互斥量?
①条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
②条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了?但是这样也会有问题
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
·由于解锁和等待不是原子操作。调用解锁之后,pthread_cond_wait之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_ cond_wait将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait。所以解锁和等待必须是一个原子操作。
·int
pthread_cond_wait;
进入该函数后,会去看条件量是否为0?等于0,就把互斥量变成1,直到cond_wait返回时,把条件量改成1,同时将互斥量恢复成原样。
所以正确是条件变量的使用规范是这样的:(这里以生产消费问题为例 简单的实现一下同步,使得消费者需要在有产品的情况下才可进行消费。)
条件变量使用范例即:
·等待条件:
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(&cond, &mutex);
//pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mutex,
//然后阻塞在等待队列里休眠,直到再次被唤醒
//(大多数情况下是等待的条件成立而被唤醒,唤醒后,
//该进程会进行pthread_mutex_lock(&mutex)先锁定,然后再读取资源
修改条件
pthread_mutex_unlock(&mutex);
·给条件发送信号代码
pthread_mutex_lock(&mutex);
//设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
多线程下的生产消费者问题
好,这下终于把准备工作做好了,结合线程的基本操作,多线程下的生产消费者我们也就不难实现出来了。如下:
/************************************************************************* > File Name: pc.c > Author: tp > Mail: > Created Time: Sun 27 May 2018 06:28:33 PM CST ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <pthread.h> #define PRO_NUM 3 //生产线程数量 #define CON_NUM 0 //消费线程数量 pthread_cond_t cond; pthread_cond_t n_empty; pthread_mutex_t mutex; int g_num = 0; //产品数量 int empty_num = 3; //生产的空位数量 //productor void* pro_route(void* arg) { int id =*(int*)arg; free(arg); while(1) { pthread_mutex_lock(&mutex); while(empty_num <= 0) { printf("生产线程%d等待。 ", id); pthread_cond_wait(&n_empty, &mutex); printf("有空位,数量为%d ", empty_num); } printf("生产线程%d生产 ", id); ++g_num; --empty_num; printf("生产品%d完成 ", g_num); sleep(rand()%3); pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); sleep(rand()%3); } } //consumer void *con_route(void* arg) { int id =*(int*)arg; free( arg); while(1) { pthread_mutex_lock(&mutex); while(g_num <= 0) { printf("消费线程%d等待。。 ", id); pthread_cond_wait(&cond, &mutex); printf("第%d产品到了!! ", g_num); } printf("消费线程%d消费 产品%d ", id , g_num); --g_num; ++empty_num; sleep(rand()%2); printf("消费线程%d消费完成 ", id); pthread_cond_signal(&n_empty); pthread_mutex_unlock(&mutex); sleep(rand()%3); } } int main( ) { srand(getpid()); pthread_t tids[PRO_NUM + CON_NUM]; //互斥量,条件变量初始化 pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond, NULL); //条件变量1 pthread_cond_init(&n_empty, NULL);//条件变量2 //创建生产者线程 for(int i =0; i< PRO_NUM; ++i) { int* p = (int*)malloc(sizeof(int)); //传入参数相当作线程编号 *p = i; pthread_create(&tids[i], NULL, pro_route, p); } //创建消费者线程 for(int i =0; i< CON_NUM; ++i) { int* p = (int*)malloc(sizeof(int)); //消费线程编号 *p = i; pthread_create(&tids[i], NULL, con_route, p); } for(int i =0; i< PRO_NUM + CON_NUM; ++i) //回收线程 { pthread_join(tids[i], NULL); } //互斥量,条件变量销毁 pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); pthread_cond_destroy(&n_empty); return 0; }
结果:
当我们只有生产者生产时,此时生产的空位生产满了之后便会阻塞,如下:
当去添加两个消费线程时(将CON_NUM改为2),这样生产、消费得以进行。如下: