在并发数据结构中,什么级别的锁定粒度是好的

What level of locking granularity is good in concurrent data structures?

本文关键字:锁定 粒度 数据结构 并发 什么      更新时间:2023-10-16

我对多线程很陌生,我有一个单线程数据分析应用程序,它具有很好的并行化潜力,虽然数据集很大,但它还没有饱和硬盘读/写,所以我认为我应该利用现在标准中的线程支持,并努力加快速度。

经过一些研究,我决定生产者-消费者是从磁盘读取数据并进行处理的好方法,我开始编写一个对象池,该对象池将成为循环缓冲区的一部分,生产者将在其中放置数据,消费者将在其中获取数据。在编写这个类时,我感觉自己在处理锁定和释放数据成员的方式上过于精细。感觉有一半的代码在锁定和解锁,而且有大量的同步对象在四处浮动。

因此,我向您介绍了一个类声明和一个示例函数,以及这个问题:这是否过于细粒度?粒度不够细?考虑不周?

struct PoolArray
{
public:
Obj* arr;
uint32 used;
uint32 refs;
std::mutex locker;
};
class SegmentedPool
{
public: /*Construction and destruction cut out*/
void alloc(uint32 cellsNeeded, PoolPtr& ptr);
void dealloc(PoolPtr& ptr);
void clearAll();
private:
void expand();
//stores all the segments of the pool
std::vector< PoolArray<Obj> > pools;
ReadWriteLock poolLock;
//stores pools that are empty
std::queue< int > freePools;
std::mutex freeLock;
int currentPool;
ReadWriteLock currentLock;
};
void SegmentedPool::dealloc(PoolPtr& ptr)
{
//find and access the segment
poolLock.lockForRead();
PoolArray* temp = &(pools[ptr.getSeg()]);
poolLock.unlockForRead();
//reduce the count of references in the segment
temp->locker.lock();
--(temp->refs);
//if the number of references is now zero then set the segment back to unused
//and push it onto the queue of empty segments so that it can be reused
if(temp->refs==0)
{
temp->used=0;
freeLock.lock();
freePools.push(ptr.getSeg());
freeLock.unlock();
}
temp->locker.unlock();
ptr.set(NULL,-1);
}

一些解释:First PoolPtr是一个愚蠢的类似指针的小对象,它将指针和指针所在池中的段号存储起来。

第二,这都是"模板化的",但我去掉了这些行,试图减少代码块的长度

第三个读写锁是我用互斥锁和一对条件变量组合在一起的。

锁无论多么细粒度都是低效的,所以要不惜一切代价避免。

队列和向量都可以使用compare-swap原语轻松实现无锁。

有很多关于的论文

无锁队列:

  • http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.53.8674&rep=rep1&type=pdf
  • http://www.par.univie.ac.at/project/peppher/publications/Published/opodis10lfq.pdf

无锁定矢量:

  • http://www.stroustrup.com/lock-free-vector.pdf

Straustrup的论文也提到了无锁分配器,但不要马上跳出来,现在标准分配器非常好。

UPD如果您不想麻烦编写自己的容器,请使用英特尔的线程构建块库,它提供线程安全矢量和队列。它们不是无锁的,但经过优化,可以有效地使用CPU缓存。

UPD关于PoolArray,您也不需要锁。如果可以使用c++11,请使用std::atomic进行原子增量和交换,否则请使用编译器内置程序(MSVC中的InterLocked*函数和gcc中的_sync*http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html)

一个好的开始-您可以在需要时锁定东西,并在完成后立即释放它们。

您的ReadWriteLock相当于一个CCriticalSection对象——根据您的需要,使用它可能会提高性能。

我想说的一件事是,在释放池poolLock.unlockForRead();上的锁之前调用temp->locker.lock();函数,否则,当池对象不受同步控制时,您将对其执行操作——此时它可能正被另一个线程使用。这是一个小问题,但对于多线程来说,最终会让你绊倒的是小问题。

多线程的一个好方法是将任何受控资源封装在对象或函数中,这些对象或函数在其中执行锁定和解锁操作,因此任何想要访问数据的人都不必担心锁定或解锁哪个锁以及何时执行。例如:

...
if(temp->refs==0)
{
temp->used=0;
freeLock.lock();
freePools.push(ptr.getSeg());
freeLock.unlock();
}
...

将是…

...
if(temp->refs==0)
{
temp->used=0;
addFreePool(ptr.getSeg());
}
...
void SegmentedPool::addFreePool(unsigned int seg)
{
freeLock.lock();
freePools.push(seg);
freeLock.unlock();
}

还有很多多线程基准测试工具。你可以用不同的方式控制你的资源,通过其中一个工具运行它,如果你觉得性能正在成为一个问题,你可以看看瓶颈在哪里。