线程互斥锁与死锁

在多线程编程中,由于线程间共享地址空间与大部分资源,而当多个线程同时访问共享数据的时候,就很有可能会发生冲突导致错误。

如下对于一个共享的全局变量进行累加,线程1与线程2均分别做累加5000次操作:

#include<stdio.h>
#include<pthread.h>

int g_a=0;

void* funtest(void* arg)
{
	int i=0;
	for(;i<5000;i++)
	{
		int tmp=g_a;
		printf("the ptheard tid is %u,the g_a is %d\n",pthread_self(),g_a);
		g_a=tmp+1;
	}
}

int main()
{
	pthread_t tid1,tid2;

	int err=pthread_create(&tid1,NULL,funtest,NULL);
	if(err!=0)
	{
		printf("pthread_create:%s\n",strerror(err));
		return -1;
	}

	err=pthread_create(&tid2,NULL,funtest,NULL);
	if(err!=0)
	{
		printf("pthread_create:%s\n",strerror(err));
		return -1;
	}


	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);

	printf("at last,g_a=%d\n",g_a);
	return 0;
}

其实,按理来说我们想的是让线程1和线程2共同对g_a进行累加5000次的操作,预想的结果应该是最后g_a的值为10000,然而,


很明显g_a的值不是10000,经过多次测试,g_a的值会变,但均在5000左右。

而出现这种现象的原因在于g_a这个变量是属于线程1和线程2所共享的,而对于g_a的累加5000次的操作都是相同的,在线程1对g_a进行累加的时候,很有可能线程2也对g_a进行了操作,而且它们的累加操作并不是原子操作,读取g_a的值,以及修改g_a的值很有可能在被线程1和线程2的共同影响下被打乱。

而对于这种对共享资源的访问冲突,我们可以通过互斥锁来进行解决,利用互斥锁将对共享资源的所有操作进行保护,使得同一时间只能有一个线程能对申请到我们的共享资源,而此时其他线程无法申请到我们的共享资源。

对于使用互斥锁,首先得创建一个互斥锁类型pthread_mutex_t的变量;

然后通过以下系统调用,进行操作:

①int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);

函数功能:对互斥锁进行初始化

参数:第一个参数表示一个指向互斥锁类型的指针,第二个参数表示互斥锁的属性,为NULL时表示缺省属性

返回值:若成功返回0,否则返回错误号

注意:若已定义的互斥锁变量为全局变量或者是静态变量,可以用PTHREAD_MUTEX_INITIALIZER这个宏来进行初始化(与pthread_mutex_init函数,第二个参数为NULL时作用相同)

②int pthread_mutex_destory(pthread_mutex_t* mutex);

函数功能:对互斥锁变量进行释放

参数:唯一参数表示一个指向互斥锁类型的指针

返回值:若成功返回0,否则返回错误号

③int pthread_mutex_lock(pthread_mutex_t* mutex);

   int pthread_mutex_trylock(pthread_mutex_t* mutex);

函数功能:两个函数均是执行加锁操作,前者当这个锁已经被其他线程所占用时,当前线程挂起等待,直到占用锁的线程退出,阻塞式;而后者则是不会挂起等待,而是返回EBUSY,非阻塞式


④int pthread_mutex_unlock(pthread_mutex_t* mutex);

函数功能:执行解锁操作,对于锁进行释放,可供其他线程申请使用

返回值:若成功则返回0,否则返回错误码


对最开始的测试用例进行修改:

#include<stdio.h>
#include<pthread.h>

int g_a=0;
pthread_mutex_t Mutex=PTHREAD_MUTEX_INITIALIZER;

void* funtest(void* arg)
{
	int i=0;
	for(;i<5000;i++)
	{
		pthread_mutex_lock(&Mutex);
		int tmp=g_a;
		printf("the ptheard tid is %u,the g_a is %d\n",pthread_self(),g_a);
		g_a=tmp+1;
		pthread_mutex_unlock(&Mutex);
	}
}

int main()
{
	pthread_t tid1,tid2;
//	pthread_mutex_init(&Mutex,NULL);

	int err=pthread_create(&tid1,NULL,funtest,NULL);
	if(err!=0)
	{
		printf("pthread_create:%s\n",strerror(err));
		return -1;
	}

	err=pthread_create(&tid2,NULL,funtest,NULL);
	if(err!=0)
	{
		printf("pthread_create:%s\n",strerror(err));
		return -1;
	}


	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
        pthread_mutex_destory(&Mutex);
	printf("at last,g_a=%d\n",g_a);
	return 0;
}
测试结果:


很明显达到预期想法。

下面对于互斥锁进行讨论一下,我们的互斥锁变量在创建时,很明显它是属于两个线程共享资源,所以对于互斥锁,想要保护我们的共享资源,首先要保护好自己,因此对于互斥锁的加锁与解锁必须保证他的操作的原子性。

对于解锁操作,无非是对一个内存单元进行赋值(由0变1),这个操作显然是原子操作。

但对于加锁操作,若是仅通过对于互斥锁变量与0进行比较,若大于0的话对变量进行赋值修改为0这样的操作的话,我们可以发现这个操作是将内存中的数据读至CPU当中,然后才进行比较,这样的操作显然不是原子操作,况且就算在比较的时候没有切换线程,只要是在对mutex赋值为0操作之前,线程被切出去了,那其他线程依旧可以申请到锁,访问临界资源


所以为了实现原子操作,可以通过下面的操作:

利用一个寄存器,将其初始化为0,利用exchange或swap指令,将我们内存中的互斥锁变量与寄存器的值进行交换,由于线程在切换时会保存上下文信息,而我们的寄存器信息就是需要被保存的一部分,所以无论线程间如何切换,寄存器的信息是不会改变的,因此下面的每一步操作都是原子操作,而且就算在期间线程切换出去,只要有人把锁申请了,mutex就会一直为0,满足我们只能让一个线程申请到锁,能够访问到临界资源的要求。



以上我们大致明白了互斥锁的原理,但是互斥锁在使用的时候,存在一个严重的问题,也就是常常提到的死锁问题。

1.首先什么是死锁?

从很多别人写的博客中,关于死锁,大致都是这么说的:线程在竞争申请一份公共资源的情况下,互相等待对方让步,而导致一直等待的问题就是死锁。

与其看这些概念,我们不如来看看死锁的两种典型情形:

①单个线程的死锁:如果一个线程对于同一把锁连续进行了两次申请,即两次lock,在第一次申请锁之后还没有进行解锁,又第二次进行申请锁,由于第一次申请锁之后还没有进行解锁,在第二次申请的时候,锁被占用着不能被再次申请,因此该线程挂起等待锁的释放,一旦挂起,那么就不可能等到解锁的时候,所以线程一直处于挂起,造成死锁

②多个线程的死锁:如果线程1申请到了A锁,线程2申请到了B锁,而在线程1对A锁进行解锁之前,又再次申请B锁,而线程2又在对B锁解锁之前,申请了A锁,那么线程1在等待线程2释放B锁,挂起等待,线程2在等待线程1释放A锁,挂起等待,两者在互相等待,就会一直处于挂起状态,造成死锁

由此,我们可以得出产生死锁的原因主要是竞争相同的资源,进程(线程)的推进顺序不当

2.造成死锁的必要条件

①互斥条件:线程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一线程所占用

②请求和保持条件:当线程因请求资源而阻塞时,对已获得的资源保持不放

③不剥夺条件:线程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放

④环路等待条件:在发生死锁时,必然存在一个线程--资源的环形链,即进程集合{P0,P1,P2…Pn}中的P0正在等待一个P1占用的资源,P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源

3.如何避免死锁

①预防死锁

资源一次性分配:对于线程请求的资源,一次性给它进行分配(破坏请求和保持条件)

可剥夺资源:当线程的新申请的资源未能满足时可以对已占有的资源进行释放(破坏不剥夺条件)

资源有序分配法:对于进程的资源进行编号,线程在申请时必须按照编号顺序进行申请(破坏环路等待条件)

但是对于上述的方法,虽能预防死锁,但是不可避免的造成对资源分配的严格限制,造成系统性能的下降

②避免死锁

允许线程动态的申请资源,最典型的算法就是银行家算法:

对于系统而言,在分配资源的之前,预先计算资源分配的安全性,若是资源分配安全,那么就分配对应资源,否则让我们的线程进行等待

(1) 当一个进程对资源的最大需求量不超过系统中的资源数时可以接纳该进程。

(2) 进程可以分期请求资源,当请求的总数不能超过最大需求量。

(3) 当系统现有的资源不能满足进程尚需资源数时,对进程的请求可以推迟分配,但总能使进程在有限的时间里得到资源。

(4) 当系统现有的资源能满足进程尚需资源数时,必须测试系统现存的资源能否满足该进程尚需的最大资源数,若能满足则按当前的申请量分配资源,否则也要推迟分配。

③检测死锁

通过系统所设置的检测机制,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,然后采取适当措施,从系统中将已发生的死锁清除掉

④解除死锁

这是与检测死锁相配套的一种措施。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资

源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行



  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值