游戏服务器之锁

本文介绍了游戏服务器常用的锁机制实现,包括互斥锁、读写锁、自旋锁、原子锁及条件变量。针对不同场景详细解释了每种锁的特点与适用情况。

游戏服务器常使用的的锁的实现  。运行平台linux。

1、互斥锁

互斥量是阻塞锁,当某线程无法获取互斥量时,该线程会被直接挂起,该线程不再消耗CPU时间,当其他线程释放互斥量后,操作系统会激活那个被挂起的线程,让其投入运行。

class nMutex  
{
public:
	friend class thread_cond;//条件变量可以操作互斥量
	// 锁类型枚举
	enum MutexType
	{
		fast = PTHREAD_MUTEX_ADAPTIVE_NP,//for "fast" mutexes
		recursive = PTHREAD_MUTEX_RECURSIVE_NP,//for "recursive" mutexes
		timed = PTHREAD_MUTEX_TIMED_NP,//for "timed" mutexes
		errorcheck = PTHREAD_MUTEX_ERRORCHECK_NP//for "error checking" mutexes
	};
	// 构造函数,构造一个互斥体对象
	nMutex(MutexType _t = fast) 
	{
		pthread_mutexattr_t attr;
		::pthread_mutexattr_init(&attr);
		::pthread_mutexattr_settype(&attr, _t);
		::pthread_mutex_init(&mutex, &attr);
	}
	//析构函数,销毁一个互斥体对象
	~nMutex() { ::pthread_mutex_destroy(&mutex); }
	//加锁一个互斥体
	void lock() { ::pthread_mutex_lock(&mutex); }
	//解锁一个互斥体
	void unlock() { ::pthread_mutex_unlock(&mutex); }
	//   试图加锁一个互斥体,return  true加锁成功, false加锁失败(已经被其他线程锁住)
	bool trylock() { return (::pthread_mutex_trylock(&mutex) == 0); }
private:
	//系统互斥体 
	pthread_mutex_t mutex;
};

 

2、读写锁

读写锁的封装。

 

class thread_rwlock : private noncopyable//noncopyable 的拷贝构造函数和等号操作符的声明是私有的
{
public:
	// 构造函数,用于创建一个读写锁
	thread_rwlock() { ::pthread_rwlock_init(&rwlock, NULL); }
	//析构函数,用于销毁一个读写锁
	~thread_rwlock() { ::pthread_rwlock_destroy(&rwlock); }
	//对读写锁进行读加锁操作
	void rdlock() { ::pthread_rwlock_rdlock(&rwlock); }
	//对读写锁进行写加锁操作
	void wrlock() { ::pthread_rwlock_wrlock(&rwlock); }
	// 对读写锁进行解锁操作
	void unlock() { ::pthread_rwlock_unlock(&rwlock); }
private:
	//系统读写锁
	pthread_rwlock_t rwlock;
};

 

 

 

3、自旋锁

 

自旋锁是一种非阻塞锁,也就是说,如果某线程需要获取自旋锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取自旋锁。

自旋锁与互斥锁区别:

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:

1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。

2、在用自旋锁时有可能造成死锁

 

两种锁适用于不同场景:

如果是多核处理器,如果预计线程等待锁的时间很短,短到比线程两次上下文切换时间要少的情况下,使用自旋锁是划算的。

如果是多核处理器,如果预计线程等待锁的时间较长,至少比两次线程上下文切换的时间要长,建议使用互斥量。

如果是单核处理器,一般建议不要使用自旋锁。因为,在同一时间只有一个线程是处在运行状态,那如果运行线程发现无法获取锁,只能等待解锁,但因为自身不挂起,所以那个获取到锁的线程没有办法进入运行状态,只能等到运行线程把操作系统分给它的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。

如果加锁的代码经常被调用,但竞争情况很少发生时,应该优先考虑使用自旋锁,自旋锁的开销比较小,互斥量的开销较大。

 

自旋锁实现如下(利用了原子操作)

 

class CSpinLock
{
public:
	enum AtomLockState
	{
		Unlocked = 0,
		Locked = 1,
	};
private:
	LONG volatile m_uLockFlag;
	DWORD volatile m_atomLockThread;
	DWORD volatile m_atomLockCount;
public:
	CSpinLock()
	{
		m_uLockFlag = Unlocked;
		m_atomLockThread = 0;
		m_atomLockCount = 0;
	}
	~CSpinLock(){}
	
	inline void CSpinLock::Lock()
	{
		DWORD currThreadId = GetCurrentThreadId();
		if ( currThreadId == m_atomLockThread )//在同一线程内加了锁,则加锁计数
		{
			m_atomLockCount++;//锁计数
		}
		else//还没加锁,或者加锁的不是同一线程
		{
			while ( Unlocked != InterlockedCompareExchange( &m_uLockFlag, Locked, Unlocked ) )//一直自旋等待锁标识打开,打开后标识加锁
			{
				Wait();
			}
			m_atomLockThread = currThreadId;
			m_atomLockCount++;
		}
	}
	inline  BOOL TryLock()
	{
		DWORD currThreadId = GetCurrentThreadId();
		if ( currThreadId == m_atomLockThread )
		{
			m_atomLockCount++;//加锁计数
		}
		else
		{
			if ( Unlocked != InterlockedCompareExchange( &m_uLockFlag, Unlocked, Unlocked ) )//如果锁标识没有打开,则加锁失败返回
				return FALSE;
			m_atomLockThread = currThreadId;
			m_atomLockCount++;
		}
		return TRUE;
	}
	inline void Unlock()
	{
		if ( GetCurrentThreadId() == m_atomLockThread )//只有是本线程加锁,才可以解锁(本线程若加了锁,在没解锁之前,其他线程不能加锁)
		{
			m_atomLockCount--;//解锁计数
			if ( !m_atomLockCount )//锁计数为0时,锁对象才标识解锁
			{
				m_uLockFlag = Unlocked;
				m_atomLockThread = 0;
			}
		}
	}
	inline void Wait(){}//等待时不做任何事
};

 

 

 

 

4、原子锁

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。

原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。 

原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1。 

 

例如:  

__sync_fetch_and_and

 

__sync_fetch_and_or

__sync_val_compare_and_swap 

__sync_fetch_and_add

 

5、条件变量

条件变量实现封装代码如下:

class thread_cond  
{
public:
	//构造函数,用于创建一个条件变量
	thread_cond()
	{
		::pthread_cond_init(&cond, NULL);
	}
	//析构函数,用于销毁一个条件变量
	~thread_cond()
	{
		::pthread_cond_destroy(&cond);
	}
	//对所有等待这个条件变量的线程广播发送信号,使这些线程能够继续往下执行
	void broadcast()
	{
		::pthread_cond_broadcast(&cond);
	}
	//对所有等待这个条件变量的线程发送信号,但是只唤醒其中的一个线程
	void signal()
	{
		::pthread_cond_signal(&cond);
	}
	
	//等待特定的条件变量满足  parammutex 需要等待的互斥体
	void wait(nMutex &mutex)
	{
		::pthread_cond_wait(&cond, &mutex.mutex);
	}
	// 可设置超时时间的等待 
	//parammutex 需要等待的互斥体
	//timeout 超时时间(单位:毫秒)
	//return true 等待事件发生
	//false 等待超时
	bool timedwait(nMutex &mutex, unsigned int timeout)
	{
		struct timeval now; // 精确到微秒
		::gettimeofday( &now, NULL );
		struct timespec tsp; // 精确到纳秒
		tsp.tv_sec = now.tv_sec + timeout / 1000;
		tsp.tv_nsec = now.tv_usec * 1000 + ( timeout % 1000 ) * 1000 * 1000;
	
		if ( 0 == ::pthread_cond_timedwait(&cond, &mutex.mutex, &tsp) )
			return true;
		else
			return false;
	}
	private:
	//系统条件变量
	pthread_cond_t cond;
};


(1)广播信号
线程中广播信号,表示线程可以回收
thread->_mlock.lock();
thread->_alive = false;
thread->_cond.broadcast();
thread->_mlock.unlock();

(2)等待信号
线程中等待信号,等到可以回收才结束
_mlock.lock();
while(_alive)//等待到_alive转为false
{
  _cond.wait(_mlock);
}
_mlock.unlock();

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值