|
关于锁的使用
在网络开发的过程当中, 相信大家经常要用到多线程.众所周知, 多线程程序需要对共享资源进行同步访问, 需要用到锁.
关于锁的使用要谨慎,稍有不对便会造成饥饿,死锁, 从而导致服务死掉.
下面我就以自己的个人体会介绍一些经验, 希望能起到抛砖引玉的作用
1. 定界锁
大家都知道, 对于同步资源的访问分为3步: 首先锁定资源, 然后访问资源, 最后释放资源, 这样就可以避免多个线程同时操作同一个资源, 等到访问结束之后, 就释放锁, 以便其他的线程能够访问该资源.事例代码如下:
LOCK lock; // LOCK为自定义的锁,实现了acquire和release接口
RESOURCE resource; // 同步资源
int ThreadFunc(void* p) //线程函数
{
lock.acquire();
//VisitResource, 此处为访问资源的代码
...
...
...
lock.release();
}
大家可能觉得上面的代码很正常, 没什么好怀疑的, 但事实上问题没这么简单, 因为在访问资源的代码中可能抛出异常, 或者产生错误而直接返回了, 这样锁就没有得到合理的释放, 因而导致了死锁.啊?正常的代码也出现了问题, 该怎么办呢?
这个问题有这样一种解决方案,
class Gurad
{
public:
GuradLOCK& lock)
{
m_pLock = &lock;
m_pLock->acquire();
}
~Gurad()
{
m_pLock->release();
}
private:
LOCK* m_pLock;
};
这样再使用同步资源的时候就变成下面这样了:
int ThreadFunc(void* p)
{
Guard temp(lock);
//访问同步资源
}
这样在函数的结尾, Guard的析构函数会自动释放锁, 即使在访问同步资源的时候出现了异常, 或者由于中间出现了错误而不经意的返回了, 也会调用Gurad的析够函数,从而保证了资源同步锁的合理释放.
(C++中的异常处理机制是基于堆栈辗转的, 也是一种变相的返回方式, 推荐大家参考下Inside the C++ Object Model).
可能会有朋友说, 如果在这个函数中我只想在某个区间内锁定, 也就是说想自主控制锁的生命期, 用玩了就释放,而不是让锁在函数区间内一直锁定, 那该怎么办呢?
让我们把这个代码再一点点完善:
class Gurad
{
public:
GuradLOCK& lock)
{
m_pLock = &lock;
m_bOwner = true;
m_pLock->acquire();
}
~Gurad()
{
if(m_bOwner)
m_pLock->release();
}
void release()
{
if(m_bOwner)
m_pLock->release();
}
private:
LOCK* m_pLock;
BOOL m_bOwner;
};
int ThreadFunc(void* p)
{
Guard temp(lock);
//访问同步资源
temp.release();
//继续进行事务处理
}
这样就可以控制锁的生命周期了.
事情好像已经很好解决了, 再想想, 如果你写的组件要用于单线程呢, 虽然只是在同步与不同步之间有一个很小的差异, 但是细节问题散落在代码各处, 这就需要重新修改代码, 编译, 调试, 很麻烦;我们知道, 单线程不需要同步, 这样就可以把单线程与多线程之间的不同点抽象出来, 用模板技术:
template < typename LOCK>
class Gurad
{
public:
Gurad(LOCK& lock)
{
m_pLock = lock;
m_bOwner = true;
m_pLock->acquire();
}
~Gurad()
{
if(m_bOwner)
m_pLock->release();
}
void release()
{
if(m_bOwner)
m_pLock->release();
}
private:
LOCK* m_pLock;
BOOL m_bOwner;
}
其中LOCK是我们自己定义的锁, 当用于多线程时, LOCK要实现对应的锁定和释放锁接口, 如果时单线程呢LOCK对应的acquire和release都是空操作 , 如下:
class NULL_LOCK
{
public:
void acquire()
{
}
void release()
{
}
}
这样在单线程和 多线程之间进行切换的时候, 只需传递给Guard模板不同的锁参数就可以了, 是不是很简单呢?
当然了, 这里涉及到基于策略的组件思想, 建议有兴趣的朋友可以研究下Modern C++ Design |
|