线程的同步和互斥
线程的同步和互斥
今天笔者和大家一起学习一下线程的同步和互斥,最简单的理解,同步就是按照某种次序去访问资源,互斥就是某种资源同时只能有一个访问者去访问,因此对于资源的访问就是原子性的。即也就是当前资源已经被占用或者没有被占用两个情况,不存在第三种情况。我们先来看一段代码:
#include <stdio.h>
#include <pthread.h>
int count = 0;
void* thread_count(void* arg)
{
int i = 0;
while(i<50000000)
{
count++;
i++;
}
}
int main()
{
pthread_t id1,id2;
pthread_create(&id1,NULL,thread_count,NULL);
pthread_create(&id2,NULL,thread_count,NULL);
pthread_join(id1,NULL);
pthread_join(id2,NULL);
printf("count:%d\n",count);
return 0;
}
我们将刚才的程序生成可执行文件之后,多运行几次,还会看见如下:此时发现有几次的运行结果不对,不是100000000,造成这个的原因就是在在thread_count函数中对于coun++不是原子性的,在两个线程都在同时运行这个功能的时候,如果循环的次数比较小,几乎不会出现该错误,是因为,在线程2还没有创建完成的时候,线程1就已经将count累加完成了,但是现在,每将功能执行一次,count就要累加50000000次,在一个线程还没有将count累加完成的时候,线程2就也开始对count进行的累加,问题就出现在这里,当两个线程同时对count进行累加,而计算机中的能够进行累加的部件就只有CPU,所以当线程1把count从内存中拿出来,放到寄存器的时候,此时会有一个上下文保护,记录count的值为0,然后放到CPU中进行累加,但是就在CPU累加count的时候,线程2,将count的值取出来,也进行累加,此时count的是值已经成为1了,但是当线程1从CPU中将累加完成之后count的值拿出来的时候,根据自己的上下文数据,认为count还是0,然后把count赋值为1,这个时候,就把线程2的累加结果覆盖掉了,这样累加的结果就小于100000000了,这样的情况对发生几次,也就造成了刚才的结果。
也正是因为累加要把数据从内存拿到CPU执行,有了一个从用户态和内核态之间转换,才造成了这样子的结果,而当累加的结果比较小的时候就不会,是因为以当前计算机的计算速度,小一点的数字,线程2还没有创建完成,此时线程1的累加工作就已经完成了,那么我们怎么在累加次数比较小的时候,让他出错呢?就是让他增加用户态和内核态的一个转化。代码如下:
#include <stdio.h>
#include <pthread.h>
int count = 0;
void* thread_count(void* arg)
{
int i = 0;
int tmp = 0;
while(i<5000)
{
tmp = count;
//printf是往stdout上输出,既然应用软件要控制硬件,必定要调用系统接口,所以printf底层一定封装了系统调用
//既然要调用系统接口,即一定得完成用户态和内核态的转换
printf("%d\n",tmp);
count = tmp+1;
i++;
}
}
int main()
{
pthread_t id1,id2;
pthread_create(&id1,NULL,thread_count,NULL);
pthread_create(&id2,NULL,thread_count,NULL);
pthread_join(id1,NULL);
pthread_join(id2,NULL);
printf("count:%d\n",count);
return 0;
}
当多次运行这段代码的时候,会发现也会出错,产生错误的原因就是临界资源的访问不是原子的,解决这个问题的方法就是:保证资源的原子性。保证资源原子性的第一种方法,就是:互斥锁,
互斥锁
怎么定一个锁呢?Linux中man手册可以查到:
现在我们在之前累加5000的代码中加入互斥锁,使得错误消失,代码如下:
#include <stdio.h>
#include <pthread.h>
int count = 0;
//创建变量,使用对应的初始化方法来进行初始化
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* thread_count(void* arg)
{
int i = 0;
int tmp = 0;
while(i<5000)
{
//加锁
pthread_mutex_lock(&lock);
tmp = count;
printf("%d\n",tmp);
count = tmp+1;
i++;
//解锁
pthread_mutex_unlock(&lock);
}
}
int main()
{
pthread_t id1,id2;
pthread_create(&id1,NULL,thread_count,NULL);
pthread_create(&id2,NULL,thread_count,NULL);
pthread_join(id1,NULL);
pthread_join(id2,NULL);
pthread_mutex_destroy(&lock);
printf("count:%d\n",count);
return 0;
}
此时,当一个线程访问临界资源的时候,其他线程因为互斥锁的原因是不能访问的,所以此时,当线程申请锁资源的时候,该线程就会被挂起,此时的状况和进程的信号量申请失败有些类似。但是还有一种情况,就是当一个优先级比较高的线程,循环的访问一块资源,也就是释放资源之后,又开始访问,这个时候,其他线程还是没有办法访问临界资源,此时解决的办法如下:
互斥锁中,每一个mutex都有一个等待队列,一个线程要在mutex上挂起等待,首先要把自己加入到等待队列中,然后状态置为睡眠,然后调用调度器函数切换别的线程,一个线程要唤起等待队列的其他线程,只需从等待队列中取出一项,把其状态从睡眠改成就绪,然后加入到就绪队列,那么下一次调度器就有可能切换到被唤起的线程,以上的动作也就是我们的“挂起等待”和“唤醒等待线程”。
死锁:
①一般情况下,如果一个线程先后两次调用lock,那么第二次调用lock的时候,由于锁已经被占用了,那么自己就会被挂起,进入到等待队列,但是锁是被自己使用的,被挂起之后也就没有机会来释放锁,此时就会永远的被挂起,造成死锁。
②当两个线程1和线程2,线程1申请使用锁1,线程2申请使用锁 2,此时线程1申请锁2,但是锁2被线程2使用,此时线程1被挂起,而同时,锁2也申请锁1 ,但是锁1 被线程1占用,且线程1已经被挂起,无法释放锁1,此时线程2也被挂起,这样锁2也无法释放,这样也就造成了死锁。
死锁产生有四种必要情况:
lock1 -> lock2 -> lock3的申请顺序获得。如果确定顺序比较困难,那么在加锁的时候尽量使用pthread_mutex_trylock代替pthread_mutex_lock。
限于编者水平,文章难免有缺漏之处,欢迎指正。
如需转载,请注明出处~!
下一篇: 简要了解JVM的内存划分