面对多个互斥量的加锁策略:"试加锁-回退"算法/固定加锁层次

有时一个互斥量是不够的:

比如:

当多个线程同时访问一个队列结构时,你需要2个互斥量,一个用来保护队列头,一个用来保护队列元素内的数据。

当为多线程建立一个树结构时,你可能需要为每个节点设置一个互斥量。

同时使用多个互斥量会导致复杂度的增加

最坏的情况就是死锁的发生,即两个线程分别锁住一个互斥量而等待对方的互斥量。


多互斥量可能导致死锁:

如果可以在独立的数据上使用两个分离的互斥量,那么就应该这么做。这样,通过减少线程必须等待其他线程完成数据操作的时间。

如果数据独立,则某个特定函数就不太可能经常需要同时加锁两个互斥量。

如果数据不是完全独立的时候,情况就复杂了。

如果你的程序中有一个不变量,影响着由两个互斥量保护的数据。

即使该不变量很少被改变或引用,你迟早需要编写同时锁住两个互斥量的代码,来确保不变量的完整性。

一个经典的死锁现象

如果一个线程锁住互斥量A后加锁互斥量B。同时另一个线程锁住互斥量B后加锁互斥量A。

这样的代码就是一个经典的死锁现象。

两个线程可能同时完成第一步。

即使是在但处理器系统中,一个线程完成了第一步后可能被时间片机制抢占,以使另一个线程完成第一步。

至此两个线程都无法完成第二步,因为他们彼此等待的互斥量已经被对方锁住。


针对上述类型的死锁,可以考虑一下两种通用的解决方法:

1、固定加锁层次

比如,所有需要同时加锁互斥量A和互斥量B的代码,必须先加锁互斥量A,再加锁互斥量B。

2、试加锁和回退

在锁住某个集合中的第一个互斥量后,使用pthread_mutex_trylock来加锁集合中的其他互斥量。

如果失败则将集合中所有已加锁的互斥量释放,并重新加锁。


固定加锁层次详解:

有许多方式定义固定加锁层次,但对于特定的互斥量,总有某个明显的加锁顺序。

例如:

如果有两个互斥量,一个保护队列头,一个保护队列元素内的数据,

则很显然的一种固定加锁层次就是先将队列头互斥量加锁,然后再加锁另一个互斥量。

如果互斥量间不存在明显的逻辑层次,则可以建立任意的固定加锁层次。

例如:

你可以创建这样一个加锁互斥量集合的函数。

将集合中的互斥量照ID地址顺序排列,并以此顺序加锁互斥量。

或者给每个互斥量指派名字,然后按照字母顺序加锁。

或者给每个互斥量指派序列号,然后按照数字顺序加锁。

从某种程度上讲,只要总是保持相同的顺序,顺序本身就并不真正重要。


试加锁和回退详解:

回退的方式没有固定加锁层次有效,它会浪费时间来试锁和回退。

另一方面,你也不必定义和遵循严格的固定加锁层次,这使得回退的方法更为灵活。

可以组合两种算法来最小化回退的代价。

即在定义良好的代码区遵循固定加锁层次,在更灵活的地方使用试加锁-回退。


试加锁和回退的代码示例:

 

#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//====================================================
#define ITERATIONS 10
//====================================================
//互斥量数组
pthread_mutex_t mutex[3]=
{
	PTHREAD_MUTEX_INITIALIZER,
	PTHREAD_MUTEX_INITIALIZER,
	PTHREAD_MUTEX_INITIALIZER
};
//====================================================
//是否开启试加锁-回退模式
int backoff=1;
/*
该标识符决定的操作:
当它>0,线程会在锁住每个互斥量后调用sched_yield,以确保其他线程有机会运行
当它<0,线程则在锁住每个互斥量后睡眠1秒,以确保其他线程真正有机会运行
*/
int yield_flag = 0;
//====================================================
void *lock_forward (void *arg)
{
	int i, iterate, backoffs;
	int status;
	//循环ITERATIONS次
	for (iterate = 0; iterate < ITERATIONS; iterate++) 
	{
		//记录回退的次数
		backoffs = 0;
		
		//按0、1、2的顺序对3个互斥量加锁
		for (i = 0; i < 3; i++) 
		{
			//按正常的方法加锁第一个互斥量
			if (i == 0) 
			{
				status = pthread_mutex_lock (&mutex[i]);
				if (status != 0)
				{	
					printf("First lock error\n");
					//终止异常程序
					abort();
				}
			}
			/*
			对于第2、3个互斥量
			如果开启了试加锁模式,就执行试加锁
			否则按照正常模式加锁
			*/
			else 
			{
				if (backoff)
					status = pthread_mutex_trylock (&mutex[i]);
				else
					status = pthread_mutex_lock (&mutex[i]);
                
				//如果是试加锁失败,则回退
				if (status == EBUSY) 
				{
					//回退次数++
					backoffs++;
					printf( "[forward locker backing off at %d]\n",i);
					
					//将之前加锁的互斥量释放掉
					for (; i >= 0; i--) 
					{
						status = pthread_mutex_unlock (&mutex[i]);
						if (status != 0)
						{	
							printf("Backoff error\n");
							//终止异常程序
							abort();
						}
					}
                }
				else 
				{
					if (status != 0)
					{
						printf("Lock mutex error\n");
						//终止异常程序
						abort();
					}
					printf("forward locker got %d\n",i);
                }
			}
            
			/*
			根据yield_flag决定是睡1秒还是调用sched_yield ()
			*/
			if (yield_flag) 
			{
				if (yield_flag > 0)
					sched_yield ();
				else
					sleep (1);
			}           
		}
		//显示加锁情况
		printf ("lock forward got all locks, %d backoffs\n", backoffs);
		//全部解锁
		pthread_mutex_unlock (&mutex[2]);
		pthread_mutex_unlock (&mutex[1]);
		pthread_mutex_unlock (&mutex[0]);
		sched_yield ();
	}
	return NULL;
}
//====================================================
void *lock_backward (void *arg)
{
	int i, iterate, backoffs;
	int status;
	//循环ITERATIONS次
	for (iterate = 0; iterate < ITERATIONS; iterate++)
	{
		
		//记录回退的次数
		backoffs = 0;
		
		//按2、1、0的顺序对3个互斥量加锁
		for (i = 2; i >= 0; i--) 
		{
			//按正常的方法加锁第一个互斥量
			if (i == 2) 
			{
				status = pthread_mutex_lock (&mutex[i]);
				if (status != 0)
				{
					printf("First lock error\n");
					//终止异常程序
					abort();
				}
			}
			/*
			对于第2、3个互斥量
			如果开启了试加锁模式,就执行试加锁
			否则按照正常模式加锁
			*/
			else 
			{
				if (backoff)
					status = pthread_mutex_trylock (&mutex[i]);
				else
					status = pthread_mutex_lock (&mutex[i]);
				
				//如果是试加锁失败,则回退
				if (status == EBUSY) 
				{
					//回退次数++
					backoffs++;
					printf( "[backward locker backing off at %d]\n",i);
					//将之前加锁的互斥量释放掉
					for (; i < 3; i++) 
					{
						status = pthread_mutex_unlock (&mutex[i]);
						if (status != 0)
						{	
							printf("Backoff error\n");
							//终止异常程序
							abort();
						}
					}
				} 
				else 
				{
					if (status != 0)
					{
						printf("Lock mutex error\n");
						//终止异常程序
						abort();
					}
					printf( "backward locker got %d\n",i);
				}
			}
			
			/*
			根据yield_flag决定是睡1秒还是调用sched_yield ()
			*/
			if (yield_flag) 
			{
				if (yield_flag > 0)
					sched_yield ();
				else
					sleep (1);
			}           
		}
		//显示加锁情况
		printf ("lock backward got all locks, %d backoffs\n", backoffs);
		//全部解锁
		pthread_mutex_unlock (&mutex[0]);
		pthread_mutex_unlock (&mutex[1]);
		pthread_mutex_unlock (&mutex[2]);
		sched_yield ();
	}
	return NULL;
}
//====================================================
int main (int argc, char *argv[])
{
	pthread_t forward, backward;
	int status;
	
	//手动设置是否开启回退模式
	if (argc > 1)
		backoff = atoi (argv[1]);
	
	//手动设置是否沉睡或调用sched_yield()
	if (argc > 2)
		yield_flag = atoi (argv[2]);
    
	//开启lock_forward线程,按0、1、2的顺序加锁互斥量
	status = pthread_create (&forward, NULL, lock_forward, NULL);
	if (status != 0)
	{
		printf("Create forward error\n");
		//终止异常程序
		abort();
	}
	
	//开启lock_forward线程,按2、1、0的顺序加锁互斥量
	status = pthread_create (&backward, NULL, lock_backward, NULL);
	if (status != 0)
	{
		printf("Create backward error\n");
		//终止异常程序
		abort();
	}
	pthread_exit (NULL);
}


 

代码结果:
[allyes_op@allyes ~]$ ./backoff 
forward locker got 1
forward locker got 2
lock forward got all locks, 0 backoffs
backward locker got 1
backward locker got 0
lock backward got all locks, 0 backoffs
forward locker got 1
forward locker got 2
lock forward got all locks, 0 backoffs
backward locker got 1
backward locker got 0
lock backward got all locks, 0 backoffs
forward locker got 1
forward locker got 2
lock forward got all locks, 0 backoffs
backward locker got 1
backward locker got 0
lock backward got all locks, 0 backoffs
forward locker got 1
forward locker got 2
lock forward got all locks, 0 backoffs
backward locker got 1
[backward locker backing off at 0]
backward locker got 1
backward locker got 0
lock backward got all locks, 1 backoffs
[forward locker backing off at 1]
forward locker got 1
forward locker got 2
lock forward got all locks, 1 backoffs
backward locker got 1
[backward locker backing off at 0]
backward locker got 1
backward locker got 0
lock backward got all locks, 1 backoffs
[forward locker backing off at 1]
forward locker got 1
forward locker got 2
lock forward got all locks, 1 backoffs
backward locker got 1
backward locker got 0
lock backward got all locks, 0 backoffs
forward locker got 1
[forward locker backing off at 2]
forward locker got 1
forward locker got 2
lock forward got all locks, 1 backoffs
[backward locker backing off at 1]
backward locker got 1
backward locker got 0
lock backward got all locks, 1 backoffs
forward locker got 1
forward locker got 2
lock forward got all locks, 0 backoffs
backward locker got 1
backward locker got 0
lock backward got all locks, 0 backoffs
forward locker got 1
forward locker got 2
lock forward got all locks, 0 backoffs
backward locker got 1
backward locker got 0
lock backward got all locks, 0 backoffs
forward locker got 1
forward locker got 2
lock forward got all locks, 0 backoffs
backward locker got 1
backward locker got 0
lock backward got all locks, 0 backoffs
[allyes_op@allyes ~]$ 


代码分析:

上述代码演示了如何使用回退算法避免互斥量死锁

程序建立了2个线程,一个运行函数lock_forward,一个运行lock_backward。

每个线程重复循环ITERATIONS,每次循环两个线程都试图以此锁住三个互斥量

lock_forward线程先锁住互斥量0,再锁住互斥量1,再锁住互斥量2。

lock_backward线程线索住互斥量2,再锁住互斥量1,再锁住互斥量0。

如果没有特殊的防范机制,则上述程序很快进入死锁状态。你可以通过[allyes_op@allyes ~]$ ./backoff 0来查看死锁的效果。

如果开启了试加锁模式

则两个线程都将调用pthread_mutex_trylock来加锁每个互斥量。

当加锁互斥量返回失败信息EBUSY时,线程释放所有现有的互斥量并重新开始。

在某些系统中,可能不会看到任何互斥量的冲突

因为一个线程总是能够在另一个线程有机会加锁互斥量之前锁住所有互斥量。

可以设置yield_flag变量来解决这个问题。

在多处理器系统中,当将yield_flag设置为非0值时,通常会看到更多的回退操作。

线程按照加锁的相反顺序释放所有锁

这是用来避免线程中的不必要的回退操作。

如果你使用“试加锁和回退”算法,你应该总是以相反的顺序解锁互斥量

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值