欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线程的同步和互斥

程序员文章站 2022-05-05 10:34:14
...

线程的同步和互斥

今天笔者和大家一起学习一下线程的同步和互斥,最简单的理解,同步就是按照某种次序去访问资源,互斥就是某种资源同时只能有一个访问者去访问,因此对于资源的访问就是原子性的。即也就是当前资源已经被占用或者没有被占用两个情况,不存在第三种情况。我们先来看一段代码:

#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也无法释放,这样也就造成了死锁。

死锁产生有四种必要情况:

1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源
如何避免死锁?
如果程序中有用到lock1,lock2,lock3,且3个锁的地址:lock1 < lock2 < lock3,那么在线程需要获得2个或者3个锁的时候,都应该按照
lock1 -> lock2 -> lock3的申请顺序获得。如果确定顺序比较困难,那么在加锁的时候尽量使用pthread_mutex_trylock代替pthread_mutex_lock。



限于编者水平,文章难免有缺漏之处,欢迎指正。
 如需转载,请注明出处~!