【问题标题】:What level of locking granularity is good in concurrent data structures?在并发数据结构中,什么级别的锁定粒度比较好?
【发布时间】:2012-12-06 08:38:50
【问题描述】:

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

经过一些研究,我认为生产者消费者是从磁盘读取数据并进行处理的好方法,我开始编写一个对象池,该对象池将成为循环缓冲区的一部分,生产者将在这里放置数据和消费者得到数据。当我写这门课时,感觉我在处理锁定和释放数据成员的方式上过于细化了。感觉有一半的代码在锁定和解锁,并且好像有大量的同步对象漂浮在周围。

所以我带着一个类声明和一个示例函数来找你,还有一个问题:这是否太细粒度了?粒度不够细?考虑不周?

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 是一个愚蠢的小指针对象,它存储指针和指针来自的池中的段号。

其次,这都是“模板化”的,但我将这些行去掉以尝试减少代码块的长度

第三个 ReadWriteLock 是我使用互斥锁和一对条件变量组合而成的。

【问题讨论】:

    标签: c++ multithreading concurrency c++11 object-pooling


    【解决方案1】:

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

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

    有很多关于这个主题的论文

    无锁队列:

    无锁矢量:

    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

    【讨论】:

    • 现在我正努力让事情变得简单,以支持多线程。我想我不得不问,我能做些什么来避免实现我自己的向量和队列,同时仍然更有效地锁定和解锁?
    • @JamesMatta,我更新了我的答案。简而言之:使用线程构建块库
    • 您不必自己实现它。那里已经有足够多的无锁数据结构了,您只需要使用它们;)这比锁定对std::vectors 和std::queues 的访问权限更容易,因为您不必费神所有的极端情况都可以弄清楚必须如何完成锁定 - 您不必找到一个您没有想到的地方,这会导致零星的竞争条件。底线:如果您必须同时使用数据结构,请重用为并发设计的现有数据结构。不要重新发明轮子。
    • @ArneMertz,虽然我同意你的观点,但我找不到任何生产就绪的无锁队列和向量实现(我的项目需要它们)。
    • @Aleguna,嗯,我喜欢它是开源的,我可能必须尝试一下,然后如果有太多用户抱怨他们必须使用的外部库的数量(我已经使用Qt 和 MPIR)然后我可能会考虑自己滚动。
    【解决方案2】:

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

    您的 ReadWriteLock 几乎是一个 CCriticalSection 对象 - 根据您的需要,使用它可能会提高性能。

    我要说的一件事是,在释放池 poolLock.unlockForRead(); 上的锁之前调用 temp-&gt;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();
    }
    

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

    【讨论】:

    • 关于解锁池段的建议现已实施,谢谢。不幸的是,谷歌搜索显示关键部分是微软的事情,不幸的是,这段代码需要在 linux 和 windows 上运行,这有点阻碍了它。还要感谢有关包装受控资源的建议。
    猜你喜欢
    • 2013-11-06
    • 2017-03-10
    • 1970-01-01
    • 1970-01-01
    • 2012-04-27
    • 2012-02-20
    • 1970-01-01
    • 2014-08-09
    • 1970-01-01
    相关资源
    最近更新 更多