多线程编程中,我们会经常碰到一个线程push信息,另外一个线程按照先进先出原则处理的情况.比如多线程资源加载,网络通信等.本文总结了此种情况下的各种解决方案,并给出一个无须加锁的解.
一.常用做法
一般来说,会采用队列+lock的方式来实现,大概代码如下:
void sendMsg(const T& tData)
{
ScopeMutex<LockType> sm(&mLock);
mVec.push(tData);
}
size_t processMsg()
{
if(mVec.empty()) return 0;
mLock.lock();
T tData = mVec.front();
mVec.pop();
mLock.unlock();
mPf(tData);
return 1;
}
Window下,一般会采用临界区来实现加锁.尤其是XP,这样细粒度的加锁/解锁会导致频繁的进入内核模式,对性能造成很大的影响.相对来说,window7好很多.
二.更好的锁
当锁粒度比较小时,采用cmpxchg指令实现的自旋锁会有更好的表现.大概逻辑如下:
Void lock()
{
If(InterlockedCompareExchange(&lockCount, 1, 0))
{
冲突空转并检测是否可以得到锁;
}
}
Void unlock()
{
lockCount = 0;
}
在了解原理后,我们也可以采用成熟库的实现.比如tbb::spin_mutex
三.减少锁的频率
常用做法里面,每次获得数据都需要加/解锁.为了减少锁的频率,我们可以采用双缓冲,一个负责接受请求,另外一个处理.改进后代码逻辑大概如下.
void sendMsg(const T& tData)
{
ScopeMutex<LockType> sm(&mLock);
mInVec->push_back(tData);
}
size_t processMsg()
{
if(mInVec->empty()) return 0;
mOutVec->erase(mOutVec->begin(), mOutVec->end());
std::swap(mInVec, mOutVec);
mLock.lock();
DataDeque& prossVec = *mOutVec;
mLock.unlock();
DataDeque::iterator ite = prossVec.begin(), iteEnd = prossVec.end();
for(; ite != iteEnd; ++ite)
mPf(*ite);
return prossVec.size();
}
四.采用并行库的队列实现
随着多核的普及,软件的并行化也越来越具有实际意义.各个厂商也在普及软件并行化方面做了很多工作.比如openMP的支持,intel的tbb库.VC2010内置的并行库.这些库本身可以方便我们的应用实现,效率方面也是做了很多。本文讨论的问题,也可以采用库里面的队列实现.比如tbb::concurrent_queue.
五.无锁实现
无锁的解决本文所提的问题,数据结构的选取很重要.必须要求push和pop操作上下文依赖尽量的少.
对于Intel486以上兼容处理器,读写4字节对齐的32位整数都是原子操作(64位类似,不过要求Pentium以上).
据此 ,单向链表能满足我们的要求.
在多线程环境下,要对单向链表操作进行适当的改进才能达到不加锁的效果.在push线程,读写链表头,另外的处理线程只读. 实际处理数据时,最后一个数据不处理.
相对于其它解决方案,无锁每次都需要额外的分配结点/处理完再释放.高并发状态下,这部分也是一个很显著的开销.我们可以维护一个跟消息列表相反的链表.让内存分配/释放都在一个线程.这样就可以用一个内存链来管理他们了.把这部分额外多出来的消耗降到最低.
如果push信息内含有需要动态分配/释放的内存,采用如上方式管理信息后,分配/释放也可以在同一线程,为优化外部内存提供了可能.
代码的核心逻辑大概如下:
inline void sendMsg(const T& tData)
{
listMy* newList = new listMy(tData);
newList->next = *m_head;
*m_head = newList;
}
inline size_t processMsg()
{
listMy* head = *m_head; // 仅仅读一次.
if(head->next) //链表是否有数据
{
listMy* recordh = head;
for(;head->next;)
{
// 链表中的最后一个总是不处理.
mPf(head->idata); // todo: del
head = head->next;
}
recordh->next = NULL;
return m_buffVec.size();
}
return 0;
}
最后附上代码链接,window/linux下均测试通过.