之前我们了解到一些线程的基本知识,线程等待,线程分离啊什么的。现在我们用这些知识简单实现一个火车站抢票的功能。
假设一共有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;
}
这样写每个线程的任务,看上去好像是没有什么问题,先看看运行结果
好像真的没有什么问题,但是这是建立在每个线程执行一个任务都是很快的情况下,我们现实中每一个买票的过程所花费的时间都不短,这可以理解成一种阻塞。我们在程序中模拟一下这个阻塞的过程,看看会出现什么结果。
不得了了,我们发现好像有几张票没卖出去,又好像1号票被卖了四次,不同的窗口出售了同样的一张票,结果出现了二义性,这个问题很严重,怎么解决呢?这就得用线程安全的知识了
通过上面的代码,我们发现多个线程同时运行的时候,在访问临界资源后,使得程序出现了二义性的结果。
线程安全就是为了解决多个线程在同时运行时,在访问临界资源的同时不能让程序出现二义性的结果。
经过这样一个模拟阻塞的过程,发现原本应该是 g_tickets = 98 的结果,却因为二义性导致结果是 g_tickets = 99。这就是由于执行流A执行的 g_tickets-- 操作是非原子的操作,也就是执行流A在执行的时候,可能会遇到时间片耗尽,从而导致执行流A被调度。相当于执行流A在执行时的任何一个地方都可能会被打断。
看着图片“形象”的再来理解一次,假设有一个厕所,互斥就是同一时间只能有一个滑稽去上厕所,其他滑稽只能在外面排队;同步就是滑稽A上完厕所后不能占着茅坑不拉* ,应该赶紧出去让出坑位给其他滑稽。
想要做到这几点,本质上就是需要一把锁,也就是互斥量 (mutex)。
互斥锁是用来保证互斥属性的一种操作
互斥锁的底层是互斥量,而互斥量**(mutex)**的本质是一个计数器,这个计数器只有两个状态,即 0 和 1 。
加锁的过程可以使用互斥锁提供的接口,以此来获取互斥锁资源
加锁操作:对互斥锁当中的互斥量保存的计数器进行减1操作
解锁操作:对互斥锁当中的互斥量保存的计数器进行加1操作
看到这里,就可以简单的理解为加锁和解锁就是这样的一个过程
那么问题来了,我们在购票代码中改变票数的操作就是 g_tickets–,这个的本质就是减一。互斥量计数器本身也是一个变量,这个变量的取值是0/1,对于这样一个变量进行加一减一操作的时候,这就是原子性操作吗?
这时候不禁想起老爹的那句话
所以说,想要解决原子性的问题,还得要用原子性的操作,怎么一步就判断有没有加锁呢?
汇编中有一个指令xchgb,可以用来交换寄存器和内存中的内容,这个操作就是原子性的,一步到位。
我们再来分析一下,如果是需要加锁的情况,互斥量计数器最后就会从1变成0;如果是不能加锁的情况,互斥量计数器中的值还是0,也就是判断之后,互斥量计数器的值都会变成0。所以我们可以在寄存器中存一个数字0,然后用这个数字0和互斥量计数器中的内容进行交换,一步到位,然后我们再根据这个寄存器交换后的值来判断加锁的情况。
当交换完毕之后,判断寄存器中的值的两种情况
再次总结一下
1.定义互斥锁
pthread_mutex_t lock;
2.初始化互斥锁
方法一:
#include
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
int pthread_mutex_lock(pthread_mutex_t *mutex);
方法二:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
所以说,一般在采用 pthread_mutex_trylock 加锁的方式时,做一个循环加锁的操作,防止因为拿不到临界资源而直接返回,进而在代码总直接访问临界资源,从而导致程序产生二义性的结果。
方法三:带有超时时间的加锁接口
#include
#include
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
#include
#include
#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
#include
#include
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;
}
查看所有线程调用堆栈的信息 --> thread apply all bt
切换到某一个执行流 --> t [执行流编号]
在某一个执行流中查看该执行流调用堆栈的信息 --> f [堆栈编号] 可以跳转到具体的堆栈
查看两个锁所占有的线程号(执行流号)
查看两个线程(执行流)发生阻塞的位置
破坏请求与保持情况:
破坏不可剥夺条件:
当线程不能获得所需要的资源时,就让这个线程陷入等待状态,在等待的时候把该线程已经占有的资源隐式的释放到系统的资源列表中,让他所占有的资源可以被其他进程使用。
这个等待的进程在他重新获得自己已有的资源以及新申请的资源才可以取消等待。
破坏循环等待条件:
采用资源有序分配,将系统中的所有资源进行顺序编号,将紧缺的,稀少的用处大的资源采用较大的编号。
在线程申请资源的时候,必须按照编号的顺序进行,一个线程只有获得较小编号的资源才可以申请较大编号的资源。