1. 与互斥相关的术语
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成(即PV操作)。
2. mutex互斥量
2.1 为什么需要互斥量?
当多个线程同时共享一个变量时,会出现前者以及修改,后者还是拿到原来数据的情况,所以我们需要一把锁,去管理这个资源,使用时,加锁,用完后,解锁。这个锁我们称之为互斥量。
2.2 Linux下互斥量的接口
2.2.1 初始化互斥量
- 静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZE
- 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数: mutex --- 要初始化的互斥量 attr:NULL
返回值: 成功返回0,失败返回错误编码
2.2.2 销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数: mutex --- 要销毁的互斥量
返回值:成功返回0,失败返回错误编码
注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
- 不要销毁一个已经加锁的互斥量 。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
2.2.3 加锁与解锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
返回值:成功返回0,失败返回错误号
调用pthread_lock()加锁时,可能会出现一些问题:
- 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
- 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥 量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
3 线程互斥
以下demo,我们模拟一个售票过程,体会一下线程互斥:
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int ticket = 100;
pthread_mutex_t mutex;
void *ticket_scalper(void *arg)
{
int id = (int*)arg;
while(1)
{
//互斥锁保护的是临界资源,加锁放在临界操作之前
//阻塞加锁----无法加锁挂起等待
//非阻塞加锁 -- 无法加锁则立即报错返回
pthread_mutex_lock(&mutex);
if(ticket > 0){
printf("scalper:%d --- get ticket:%d\n",id,ticket);
ticket--;
}
else{
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
int i,ret;
pthread_t tid[4];
pthread_mutex_init(&mutex,NULL);
for(i = 0; i < 4; i++)
{
ret = pthread_create(&tid[i],NULL,ticket_scalper,(void*)i);
if(ret != 0){
printf("create falied!!\n");
return -1;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
4.cond条件变量
4.1 什么是条件变量?
条件变量是利用线程间共享的全局变量进行同步的一种机制。当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。、
4.2 Linux下条件变量的接口
4.2.1 初始化/销毁
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数: cond --- 要初始化的条件变量 attr --- NULL
返回值: 成功返回0,失败返回错误编码
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
4.2.2 等待条件/唤醒等待
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
参数: cond --- 要在这个条件变量上等待 mutex --- 互斥量
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒全部
int pthread_cond_signal(pthread_cond_t *cond); //唤醒单个线程
5. 线程同步
5.1 同步与静态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
来看以下demo,来体会线程同步:
我们模拟了厨师做面,客户吃面的过程
对于客户:没面时等待,有面时吃面,并唤醒厨师进程。
对于厨师:有面时等待,没面时做面,并唤醒客户进程。
#include <stdio.h>
#include<unistd.h>
#include<pthread.h>
int noodles = 0; //默认没有noddles
pthread_cond_t cond_foodie;
pthread_cond_t cond_chef;
pthread_mutex_t mutex;
void* thr_chef(void* arg)
{
while(1){
//没有面则做面,有面没人吃则等待
while(noodles == 1){
pthread_cond_wait(&cond_chef,&mutex);
pthread_mutex_lock(&mutex);
}
printf("master made a bow of noodles\n");
noodles = 1;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_foodie);
}
return NULL;
}
void* thr_foodie(void* arg)
{
while(1){
//有面吃面,没面则等待
while(noodles == 0)
{
pthread_cond_wait(&cond_foodie,&mutex);
pthread_mutex_lock(&mutex);
}
printf("have eat\n");
noodles = 0;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_chef);
}
return NULL;
}
int main()
{
int ret,i;
pthread_t ctid[4],etid[4];
pthread_cond_init(&cond_foodie,NULL);
pthread_cond_init(&cond_chef,NULL);
pthread_mutex_init(&mutex,NULL);
for(i = 0; i < 4; i++){
ret = pthread_create(&ctid[i],NULL,thr_chef,NULL);
if(ret != 0){
printf("create error\n");
return -1;
}
}
for(i = 0; i < 4; i++){
ret = pthread_create(&etid[i],NULL,thr_foodie,NULL);
if(ret != 0){
printf("create error\n");
return -1;
}
}
for(i = 0; i < 4; i++){
pthread_join(ctid[i],NULL);
pthread_join(etid[i],NULL);
}
pthread_cond_destroy(&cond_foodie);
pthread_cond_destroy(&cond_chef);
pthread_mutex_destroy(&mutex);
return 0;
}
在上面的demo里,我们可以看到条件变量的使用其实都是配合mutex同时使用。
为什么pthread_cond_wait需要互斥量?
如果多个线程同时唤醒一个wait,那么必然会出现race condition(竞态条件),所以没有互斥锁就无法保证安全的访问共享数据。