线程的同步与互斥
互斥量(mutex)
在这里,我们先看一个代码。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100;
void* BuyTicket(void* arg)
{
char* msg = (char*)arg;
while(1) {
if(ticket > 0) {
usleep(1000);
printf("%sBuy ticket,have tickets %d\n",msg, ticket);
ticket--;
} else {
break;
}
}
return (void*)0;
}
int main()
{
pthread_t tid1, tid2, tid3, tid4;
pthread_create(&tid1, NULL, BuyTicket, (void*)"thread1");
pthread_create(&tid2, NULL, BuyTicket, (void*)"thread2");
pthread_create(&tid3, NULL, BuyTicket, (void*)"thread3");
pthread_create(&tid4, NULL, BuyTicket, (void*)"thread4");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
return 0;
}
我们发现,票数竟然还有负的,这显然与我们预计的不同。那是什么原因呢?我们创建四个线程,这四个线程之间互相争抢ticket,每抢一张后,会将总票数减减,直至抢完。此时由于我们对ticket的减减并不是原子操作,(首先将全局变量ticket从内存加载到寄存器中,接着减一更新寄存器里面的值,最后将更新过后的新值放入内存)而我们在线程执行“抢票”操作的时候,引入usleep,如果不usleep,可能票直接就被第一个线程全部抢光了。所以引入usleep是为了在usleep的时候让别的线程可以进入,达到所有线程都在争抢这个临界资源ticket。
相当于100张票,线程一拿的时候是100张,然后在线程一还没完成抢票的时候,线程二完成抢票此时ticket剩余99张,但是线程一不知道,所以它默认100张票,它抢完以后还剩99,它将99放入内存。这时候相当于抢了两张票,但是ticket只减了一,这时候就出现了问题。
所以这时候我们要解决这个问题就要做到:当某一个线程访问临界资源的时候,不能让其他的线程再次访问临界资源,一次有且只能有一个线程去访问临界资源。所以相当于有一把锁,任意一个线程在访问临界资源时,首先申请锁,如果申请到锁,那么它将访问临界资源,并且其他进程无法申请到锁,只能进行阻塞等待,直至申请到锁的线程访问完毕,并且释放锁,其他进程才可申请到锁。Linux上提供的这把锁就叫做互斥量。
互斥量的基本函数:
pthread_mutex_t mutex;//定义互斥量
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
//功能:初始化互斥量
//参数:mutex要初始化的互斥量,attr:NULL
int pthread_mutex_destroy(pthread_mutex_t* mutex);
//功能:销毁互斥量
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
//互斥量加锁与解锁
销毁互斥量注:
- 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,在后面是不能尝试再次加锁
加锁注:
- 在线程申请互斥量时,其他线程已经申请并加锁,未解锁,或未竞争过其它线程时,那么pthread_lock调用会陷入阻塞,等待互斥量解锁。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t mutex;//定义互斥量
void* BuyTicket(void* arg)
{
char* msg = (char*)arg;
while(1) {
pthread_mutex_lock(&mutex);//加锁
if(ticket > 0) {
usleep(1000);
printf("%sBuy ticket,have tickets %d\n",msg, ticket);
ticket--;
pthread_mutex_unlock(&mutex);//解锁并释放
} else {
pthread_mutex_unlock(&mutex);//解锁并释放
break;
}
}
return (void*)0;
}
int main()
{
pthread_t tid1, tid2, tid3, tid4;
pthread_mutex_init(&mutex, NULL);//初始化互斥量
pthread_create(&tid1, NULL, BuyTicket, (void*)"thread1");
pthread_create(&tid2, NULL, BuyTicket, (void*)"thread2");
pthread_create(&tid3, NULL, BuyTicket, (void*)"thread3");
pthread_create(&tid4, NULL, BuyTicket, (void*)"thread4");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
pthread_join(tid4, NULL);
return 0;
}
我们发现假如互斥锁以后,这个时候我们之前出的问题就不复存在了。
条件变量
前面的互斥锁目的是在某个线程访问临界资源的时候,在它没有访问完毕释放锁之前,其它线程是无法申请到互斥锁的。倘若现在的情景不是在买票,而是在超市购物,消费者去超市购物,想要买盒酸奶,到了货架上发现酸奶没有了。这时候消费者就会问促销员有没有酸奶。促销员说没有。如果这时候消费者不停的一直问一直问,相当于线程不断的申请到锁访问临界资源。那么作为促销员他都没有时间去仓库通知上货。那这种情况只能是永无止境的问,永无止境的等待。
相当于一个线程在临界资源中存放数据,而另一个线程在临界资源中读取数据。而读取数据的线程优先级更高,它申请到锁的可能更大。如果此时临界资源没有数据可以读,但是由于线程优先级高,它不断的申请锁释放锁,但是实际上它并没有拿到任何资源。并且存放数据的线程还申请不到锁,无法进去放置资源,这时候两个线程都做不了自己要做的事情,这种情况就叫做发生了死锁问题。
那么如何避免这个问题呢?这时候就需要引进条件变量。
条件变量的作用就是在读数据的线程申请到锁进入临界资源,发现并没有数据是。这时候触发条件变量,释放锁,并挂起等待。直到放数据的线程申请到锁,并存放数据完毕后,唤醒读数据的线程。依次循环。
条件变量函数
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);
//功能:初始化条件变量
//参数:cond:要初始的条件变量
//attr:NULL
int pthread_cond_destroy(pthread_comd_t* cond);
//功能:销毁条件变量
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
//功能:等待条件满足
//参数:cond:要在这个条件变量下等待
//mutex:互斥量,要等待的互斥量
int pthread_cond_broadcat(pthread_cond_t* cond);//唤醒一群线程
int pthread_cond_signal(pthread_cond_t* cond);//唤醒某一个线程
在验证条件变量的使用之前,先引入一个叫做生产者消费者模型。生产者是作为产生资源,消费者是取走资源。有一片区域是提供的交易场所。生产者与消费者在这片区域交易。同时如果有很多生产者,那么这些生产者之间的关系就是互斥,而消费者与消费者之间也是互斥。每个生产者与单个消费者之间的关系是互斥且同步。所以是三种关系两个角色一个交易场所。引入生产者消费者模型后,我们利用这个来验证一下条件变量。
#include <stdio.h>
#include "seqqueue.h"
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
void* consumers(void* arg)
{
SeqQueue* q = (SeqQueue*)arg;
char top = '\0';
while(1) {
pthread_mutex_lock(&mutex);
usleep(500);
int ret = SeqQueueGetFront(q, &top);
if(ret == -1) {
printf("can not get\n");
//pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
} else {
SeqQueuePop(q);
printf("consumers get data %c\n",top);
pthread_mutex_unlock(&mutex);
}
}
}
void* producers(void* arg)
{
SeqQueue* q = (SeqQueue*)arg;
char i = 'a';
while(1) {
pthread_mutex_lock(&mutex);
SeqQueuePush(q, i);
printf("producers set data %c\n",i);
i++;
sleep(1);
//pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tid1,tid2;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
SeqQueue q;
SeqQueueInit(&q);
pthread_create(&tid1, NULL, consumers, (void*)&q);
pthread_create(&tid2, NULL, producers, (void*)&q);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
当我们取消掉条件变量后,发现这个时候结果大相径庭。这时候就体会到了条件变量的作用。
欢迎大家共同讨论,如有错误及时联系作者指出,并改正。谢谢大家!