临界资源
进行争抢访问,而不会造成数据二义性或者逻辑混乱,称这段争抢访问的过程是线程安全的。线程安全的实现:
同步
:通过条件判断,实现对临界资源访问的时序合理性互斥
:通过唯一访问,实现对临界资源访问的安全性互斥锁
、信号量
临界资源
进行访问之前先进行状态判断,决定是否能够访问,不能访问则使其休眠。计数器
,只有0/1的计数器,用于标记资源当前的访问状态原子性
:结论:
pthread_mutex_t mutex;
mutex=PTHREAD_MUTEX_INITIALIZER /
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
mutex:互斥锁变量首地址
attr:互斥锁属性,通常置NULL
int pthread_mutex_lock(pthread_mutex_t *mutex);// 阻塞加锁----如果不能加锁,则一直等待
int pthread_mutex_trylock(pthread_mutex_t *mutex);// 非阻塞加锁----如果不能加锁,则立即报错返回;若可以加锁,则加锁后返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
以黄牛抢票为例:
不加锁的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int g_tickets = 100; // 总共有100张票
void *thr_Scalpers(void *arg)
{
while(1)
{
if(g_tickets > 0)
{
usleep(1000);
printf("I : [%p] got a ticket : %d\n", pthread_self(), g_tickets);
g_tickets--;
}
else
{
pthread_exit(NULL); // 票抢完退出
}
}
return NULL;
}
int main()
{
int i = 0;
pthread_t tid[4]; // 4个线程(黄牛)
for(i = 0; i < 4; ++i)
{
int ret = pthread_create(&tid[i], NULL, thr_Scalpers, NULL);
if(ret != 0)
{
printf("thread create error!\n");
return -1;
}
}
for(i = 0; i < 4; ++i)
{
pthread_join(tid[i], NULL);
}
return 0;
}
结果:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int g_tickets = 100;
// 1.定义互斥锁变量,并且这个变量也额是一个临界资源,需要能够被各个线程访问到
// 可以将互斥锁变量定义为一个全局变量,也可以将其定义成局部变量,然后通过函数传参传递给线程
pthread_mutex_t mutex;
void *thr_Scalpers(void *arg)
{
// 尽量避免对不需要加锁的操作进行加锁,会影响效率,因为在加锁期间别人都不能操作
// 若加锁的操作越多,意味着需要的时间就更长
while(1)
{
pthread_mutex_lock(&mutex); // 加锁一定是在临界资源访问之前,保护的也仅仅是临界区
if(g_tickets > 0)
{
usleep(1000);
printf("I : [%p] got a ticket : %d\n", pthread_self(), g_tickets);
g_tickets--;
// 解锁是在临界资源访问完毕之后
pthread_mutex_unlock(&mutex);
usleep(1000); // 防止一个黄牛抢完马上又抢(一个线程分到时间片,解锁后立马又加锁)
}
else
{
// 在任意有可能退出线程的地方,记得解锁
// 若退出没有解锁,则其他线程获取不到锁,就会卡死
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
}
return NULL;
}
int main()
{
int i = 0;
pthread_t tid[4];
// 2.初始化互斥锁,一定要在创建线程之前!
pthread_mutex_init(&mutex, NULL);
for(i = 0; i < 4; ++i)
{
int ret = pthread_create(&tid[i], NULL, thr_Scalpers, NULL);
if(ret != 0)
{
printf("thread create error!\n");
return -1;
}
}
for(i = 0; i < 4; ++i)
{
pthread_join(tid[i], NULL);
}
// 3.销毁互斥锁,释放资源
pthread_mutex_destroy(&mutex);
return 0;
}
结果:
死锁:多个执行流对锁资源进行争抢访问,但因为推进顺序不当,而导致相互等待,最终造成程序流程无法继续的情况
互斥条件
:一个资源每次只能被一个执行流使用请求与保持条件
:一个执行流因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件
:一个执行流已获得的资源,在末使用完之前,不能强行剥夺循环等待条件
:若干执行流之间形成一种头尾相接的循环等待资源的关系在编码过程中注意破坏死锁产生的必要条件
等待队列
pthread_cond_t cond;
pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr); /
cond=PTHREAD_COND_INITIALIZER;
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);// 阻塞等待
pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
struct timespec *abstime); //限制等待时长的阻塞操作:等待一段指定的时间,
//时间到了调用就会报错返回----ETIMEOUT
pthread_cond_signal(pthread_cond_t *cond); // 唤醒至少一个等待的线程
pthread_cond_broadcast(pthread_cond_t *cond); // 唤醒所有等待的线程
pthread_cond_destroy(pthread_cond_t *cond);
以顾客和厨师为例子:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int bowl = 0; // 碗初始为0,表示没有饭
pthread_mutex_t mutex;
pthread_cond_t cond;
void *thr_customer(void *arg)
{
// 这是一个顾客流程
while(1)
{
// 0.加锁
pthread_mutex_lock(&mutex);
if(bowl == 0)
{
// 如果没有饭,则要等待,因为已经加过锁了,所以等待之前要解锁,被唤醒之后要加锁
// 因此pthread_cond_wait集合了三步操作:解锁/挂起/加锁
// 解锁和挂起是一个原子操作-不可被打断
// 顾客解锁,还没来得及挂起休眠,这时候厨师来做饭,做好后唤醒顾客(实际顾客还没休眠)
pthread_cond_wait(&cond, &mutex);
}
printf("good tast!\n");
bowl = 0; // 饭被吃完
// 唤醒厨师,再做一碗
pthread_cond_signal(&cond);
// 解锁操作
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *thr_cook(void *arg)
{
// 这是一个厨师流程
while(1)
{
// 0.加锁操作,因为要对碗进行操作
pthread_mutex_lock(&mutex);
if(bowl == 1)
{
// 有饭,陷入等待
pthread_cond_wait(&cond, &mutex);
}
printf("cook...\n");
bowl = 1; // 做了一碗饭
// 唤醒顾客
pthread_cond_signal(&cond);
// 解锁操作
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t ctid[2];
int ret;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
ret = pthread_create(&ctid[0], NULL, thr_customer, NULL);
if(ret != 0)
{
printf("create customer failed!\n");
return -1;
}
ret = pthread_create(&ctid[1], NULL, thr_cook, NULL);
if(ret != 0)
{
printf("create cook failed!\n");
return -1;
}
pthread_join(ctid[0], NULL);
pthread_join(ctid[1], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
分析:
- 假设有三个顾客和三个厨师,顾客先进,由于碗空,三个顾客休眠进入等待队列;
- 厨师A拿到了碗,另外两个厨师在锁上等待;
- 厨师A做完饭,唤醒顾客吃饭并解锁;
- 另外两个厨师发现碗里有饭也进入等待队列;
- 假设顾客A和B被唤醒吃饭,此时只有顾客A能吃,顾客B在锁上等待;
- 顾客A吃完后唤醒厨师,解锁;厨师被唤醒,但是不一定有时间片,解锁之后有可能顾客B抢到锁,此时碗里并没有饭,但顾客B依然吃饭了(吃了没有的饭) ,此时逻辑混乱。
所以判断碗是否有饭应该是一个循环判断
,在有多个顾客的情况下,大家都争抢锁,拿到锁之后再重新判断一次有没有饭,有饭则不再等待,吃饭;没有饭则重新调用pthread_cond_wait休眠。
此时逻辑正确了,但还有问题
此时pcb等待队列中,既有顾客pcb也有厨师pcb;
顾客吃完饭后要去队列中唤醒厨师;
但大家在同一队列中,因此有可能唤醒的不是厨师,而是另一个顾客;唤醒角色错误,顾客因为没有饭休眠,程序卡死。
所以不同的角色应该等待在不同的队列中。
即程序中角色有多少种,条件变量就应该有多少个,各自等待在自己的队列中。
修改后的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int bowl = 0; // 碗初始为0,表示没有饭
pthread_mutex_t mutex;
pthread_cond_t consumer_cond;
pthread_cond_t cook_cond;
void *thr_customer(void *arg)
{
// 这是一个顾客流程
while(1)
{
// 0.加锁
pthread_mutex_lock(&mutex);
while(bowl == 0)
{
// 如果没有饭,则要等待,因为已经加过锁了,所以等待之前要解锁,被唤醒之后要加锁
// 因此pthread_cond_wait集合了三步操作:解锁/挂起/加锁
// 解锁和挂起是一个原子操作-不可被打断
// 顾客解锁,还没来得及挂起休眠,这时候厨师来做饭,做好后唤醒顾客(实际顾客还没休眠)
pthread_cond_wait(&consumer_cond, &mutex);
}
printf("good tast!\n");
bowl = 0; // 饭被吃完
// 唤醒厨师,再做一碗
pthread_cond_signal(&cook_cond);
// 解锁操作
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *thr_cook(void *arg)
{
// 这是一个厨师流程
while(1)
{
// 0.加锁操作,因为要对碗进行操作
pthread_mutex_lock(&mutex);
while(bowl == 1)
{
// 有饭,陷入等待
pthread_cond_wait(&cook_cond, &mutex);
}
printf("cook...\n");
bowl = 1; // 做了一碗饭
// 唤醒顾客
pthread_cond_signal(&consumer_cond);
// 解锁操作
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t ctid[2];
int ret;
int i = 0;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cook_cond, NULL);
pthread_cond_init(&consumer_cond, NULL);
for(i = 0; i < 4; ++i)
{
ret = pthread_create(&ctid[0], NULL, thr_customer, NULL);
if(ret != 0)
{
printf("create customer failed!\n");
return -1;
}
}
for(i = 0; i < 4; ++i)
{
ret = pthread_create(&ctid[1], NULL, thr_cook, NULL);
if(ret != 0)
{
printf("create cook failed!\n");
return -1;
}
}
pthread_join(ctid[0], NULL);
pthread_join(ctid[1], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cook_cond);
pthread_cond_destroy(&consumer_cond);
return 0;
}
解锁
+ 休眠
+ (被唤醒后)加锁
,解锁和休眠是一个连在一起的原子操作)