游戏服务器常使用的的锁的实现 。运行平台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();