之前我们了解到一些线程的基本知识,线程等待,线程分离啊什么的。现在我们用这些知识简单实现一个火车站抢票的功能。
假设一共有100张票,我们开放4个窗口(线程),让每个窗口进行卖票的功能,每个窗口之间是的,他们的任务就是卖完这100张票,每卖一张票,就让总票数-1。
void* ThreadStart(void* arg)
{
(void*)arg;
while(1)
{
if(g_tickets > 0)
{
g_tickets--; //总票数-1
//usleep(100000); //模拟一个窗口的堵塞
printf("i am thread [%p],I'll sell ticket number [%d]\n",\
pthread_self(),g_tickets + 1);
}
else
{
break;//没有票了,直接返回该线程
}
}
return NULL;
}
这样写每个线程的任务,看上去好像是没有什么问题,先看看运行结果
通过上面的代码,我们发现多个线程同时运行的时候,在访问临界资源后,使得程序出现了二义性的结果。
线程安全就是为了解决多个线程在同时运行时,在访问临界资源的同时不能让程序出现二义性的结果。
经过这样一个模拟阻塞的过程,发现原本应该是 g_tickets = 98 的结果,却因为二义性导致结果是 g_tickets = 99。这就是由于执行流A执行的 g_tickets-- 操作是非原子的操作,也就是执行流A在执行的时候,可能会遇到时间片耗尽,从而导致执行流A被调度。相当于执行流A在执行时的任何一个地方都可能会被打断。
想要做到这几点,本质上就是需要一把锁,也就是互斥量 (mutex)。
互斥锁是用来保证互斥属性的一种操作
互斥锁的底层是互斥量,而互斥量**(mutex)**的本质是一个计数器,这个计数器只有两个状态,即 0 和 1 。
加锁的过程可以使用互斥锁提供的接口,以此来获取互斥锁资源
加锁操作:对互斥锁当中的互斥量保存的计数器进行减1操作
解锁操作:对互斥锁当中的互斥量保存的计数器进行加1操作
看到这里,就可以简单的理解为加锁和解锁就是这样的一个过程
这时候不禁想起老爹的那句话
当交换完毕之后,判断寄存器中的值的两种情况
再次总结一下
1.定义互斥锁
pthread_mutex_t lock;
2.初始化互斥锁
方法一:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
方法二:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //pthread_mutex_initializer
pthread_mutexattr_t 本身是一个结构体的类型,我们可以用 PTHREAD_MUTEX_INITIALIZER 宏定义一个结构体的值,使用这种初始化的方法可以直接填充 pthread_mutexattr_t 这个结构体
3.加锁
方法一:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
方法二:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
所以说,一般在采用 pthread_mutex_trylock 加锁的方式时,做一个循环加锁的操作,防止因为拿不到临界资源而直接返回,进而在代码总直接访问临界资源,从而导致程序产生二义性的结果。
方法三:带有超时时间的加锁接口
#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
4.解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
不管是对 pthread_mutex_lock ,pthread_mutex_trylock,还是pthread_mutex_timedlock进行加锁操作,使用该函数都可以进行一个解锁,“”。
5.销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥锁销毁接口,如果使用互斥锁完成之后,如果不调用销毁接口,就会造成内存泄漏的问题。
我们再来完善一下那个买票的程序
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define THREADNUM 4 //4个线程来当做购票的窗口
int g_tickets = 100; //100张票
pthread_mutex_t lock; 定义一个互斥锁变量
void* ThreadStart(void* arg)
{
(void*)arg;
while(1)
{
pthread_mutex_lock(&lock);
if(g_tickets > 0)
{
g_tickets--; //总票数-1
usleep(10000); //模拟一个
printf("i am thread [%p],I'll sell ticket number [%d]\n",\
pthread_self(),g_tickets + 1);
}
else
{
//假设有一个执行流判断了g_tickets之后发现,g_tickets的值是小于等于0的
//则会执行else逻辑,直接就被break跳出while循环
//跳出while循环的执行流还加着互斥锁
//所以在所有有可能退出线程的地方都需要进行解锁操作
pthread_mutex_unlock(&lock);
break;//没有票了,直接返回该线程
}
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main()
{
pthread_mutex_init(&lock,NULL);//创建线程之前进行初始化
pthread_t tid[THREADNUM];//保存线程的标识符
int i = 0;
for(i = 0; i < THREADNUM; i++)
{
int ret = pthread_create(&tid[i],NULL,ThreadStart,NULL);
if(ret < 0)
{
perror("pthread_create error\n");
return 0;
}
}
sleep(1);
for(i = 0; i < THREADNUM; i++)
{
//线程等待
pthread_join(tid[i],NULL);
}
//锁销毁
pthread_mutex_destroy(&lock);
return 0;
}
对第二种产生死锁方式的解释
假设有两个执行流(执行流A,执行流B),两个互斥锁(互斥锁1,互斥锁2)。
两个线程任务的第一步就是上锁,执行流A先获取互斥锁1,执行流B获取互斥锁2。
第二步,执行流A在已经上锁了互斥锁1的条件下,想要想要获取互斥锁2;与此同时,执行流B又在上锁了互斥锁2的条件下想要获取互斥锁1。两个执行流第二步想要获取的互斥锁都处于上锁的状态,同时两个执行流都处于无法停止的任务中,也就是阻塞状态。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t lock1;
pthread_mutex_t lock2;
void* ThreadA(void* arg)
{
(void)arg;
//设置进程属性为结束后自动释放进程空间
pthread_detach(pthread_self());
//获取互斥锁1
pthread_mutex_lock(&lock1);
sleep(3);
//获取互斥锁2
pthread_mutex_lock(&lock2);
//解锁
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
return NULL;
}
void* ThreadB(void* arg)
{
(void)arg;
pthread_detach(pthread_self());
//获取互斥锁2
pthread_mutex_lock(&lock2);
sleep(3);
//获取互斥锁1
pthread_mutex_lock(&lock1);
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}
int main()
{
//互斥锁初始化
pthread_mutex_init(&lock1,NULL);
pthread_mutex_init(&lock2,NULL);
pthread_t tid[2];//模拟两个执行流
//创建两个线程
int ret = pthread_create(&tid[0],NULL,ThreadA,NULL);
if(ret < 0)
{
perror("pthread_create A error");
}
ret = pthread_create(&tid[1],NULL,ThreadB,NULL);
if(ret < 0)
{
perror("pthread_create B error");
}
//主线程进行等待
while(1)
{
sleep(1);
printf("i am main thread\n");
}
//互斥锁销毁
pthread_mutex_destroy(&lock1);
pthread_mutex_destroy(&lock2);
return 0;
}
再用gdb 调试一下,看看线程阻塞的地方
查看两个锁所占有的线程号(执行流号)
查看两个线程(执行流)发生阻塞的位置
破坏请求与保持情况:
破坏不可剥夺条件:
当线程不能获得所需要的资源时,就让这个线程陷入等待状态,在等待的时候把该线程已经占有的资源隐式的释放到系统的资源列表中,让他所占有的资源可以被其他进程使用。
这个等待的进程在他重新获得自己已有的资源以及新申请的资源才可以取消等待。
破坏循环等待条件:
采用资源有序分配,将系统中的所有资源进行顺序编号,将紧缺的,稀少的用处大的资源采用较大的编号。
在线程申请资源的时候,必须按照编号的顺序进行,一个线程只有获得较小编号的资源才可以申请较大编号的资源。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- igbc.cn 版权所有 湘ICP备2023023988号-5
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务