【问题标题】:Can someone help spot the errors in my low lock list?有人可以帮助找出我的低锁定列表中的错误吗?
【发布时间】:2010-12-25 17:46:15
【问题描述】:

我在 32 位 Windows 上用 C++ 编写了一个低锁定列表。与使用关键部分相比,我得到了很大的改进,但我希望有人能理智地检查我所做的事情是否正确,并且我所做的事情没有任何错误:

#ifndef __LOW_LOCK_STACK_H_
#define __LOW_LOCK_STACK_H_

template< class T > class LowLockStack
{
protected:
    struct Entry
    {
        Entry*  pNext;
        T*              pData;
    };
    union Header
    {
        __int64         m_XChg;
        struct
        {
                Entry*  m_pNext;
                __int16 m_Depth;
                __int16 m_Counter;
        };
    };

    Header      m_Header;
public:
    LowLockStack()
    {
        m_Header.m_pNext        = NULL;
        m_Header.m_Depth        = 0;
        m_Header.m_Counter  = 0;
    }

    ~LowLockStack()
    {
    }

    void PushEntry( T* pData )
    {
        Entry* pEntry   = new Entry;
        pEntry->pData   = pData;

        Header header;
        Header xchg;
        do
        {
            xchg.m_XChg   = m_Header.m_XChg;

            header.m_pNext  = pEntry;
            header.m_Depth  = xchg.m_Depth + 1;
            header.m_Counter = xchg.m_Counter + 1;
            pEntry->pNext  = xchg.m_pNext;
        } while( _InterlockedCompareExchange64( &m_Header.m_XChg, header.m_XChg, xchg.m_XChg ) != xchg.m_XChg );
    }

    T* PopEntry()
    {
        Entry* pEntry   = NULL;
        Header header;
        Header xchg;
        do
        {
            xchg.m_XChg    = m_Header.m_XChg;

            pEntry                  = xchg.m_pNext;
            if ( pEntry == NULL )
            {
                 return NULL;
            }

            header.m_pNext  = pEntry->pNext;
            header.m_Depth  = xchg.m_Depth - 1;

        } while( _InterlockedCompareExchange64( &m_Header.m_XChg, header.m_XChg, xchg.m_XChg ) != xchg.m_XChg );

        T* pRet = pEntry->pData;
        delete pEntry;

        return pRet;
    }

    __int32 GetDepth()
    {
        return m_Header.m_Depth;
    }
};

#endif

如果没有错误(我怀疑;))然后将其视为参考实现:D

编辑:考虑到一些批评,我已经更新了代码。

【问题讨论】:

  • 你能量化你在关键部分看到的性能增益吗?访问列表的竞争程度如何?有可能获得比无锁更好的性能(例如,每线程队列)。
  • 量化性能增益?我没有做足够的测试来量化它。我可以说的是,当我在问题上投入更多线程时,使用 CriticalSections 性能会降低。事实上,单线程提供了迄今为止最好的性能。使用新系统时,代码的线程部分从大约 0.6 秒减少到大约 0.2 秒,当拆分为 4 个线程时。不完美,但对旧系统的改进是地狱般的,我相信你会同意的。
  • 当然,由于每个任务的执行速度,争用问题很明显。我不知道对 12 个功能的 16384 个 MFCC 进行日志测试可能会如此之快。旧代码大部分时间都在争夺锁。显然,运行速度较慢的任务会减少争用...

标签: c++ winapi stack interlocked


【解决方案1】:

最明显的错误是你给它的名字。不管您将它实现为 一个链表,但您实现的 一个堆栈。

【讨论】:

  • 改用 cmpxchg16b 有什么问题?这将解决 64 位问题...
  • @Goz:cmpxchg16b 很好,除了 VC++ 没有为它提供直接的前端并且并非所有处理器都包含它。在 VC++ 中,您将获得 InterlockedCompare64Exchange128。我认为这对于您现在正在做的事情已经足够了,但是您需要注意只需要比较 64 位,即使您交换的是 128 位。
  • @Goz:哎呀——不知何故我错过了。谢谢指点。
【解决方案2】:

考虑当列表(实际上是堆栈)中有两个项目 A 和 B 时,在以下事件序列中会发生什么,例如 head -&gt; A -&gt; Bcount 是 2:

  1. 线程 1 启动 pop() 调用,但在 _InterlockedCompareExchange64() 之前被抢占
  2. 线程 2 从堆栈中删除两个项目,A 和 B,然后将两个新项目放回堆栈,并且顶部项目恰好分配在与 A 相同的地址,所以我们有,比如head -&gt; A -&gt; D。请注意,count 回到 2
  3. 线程 1 恢复并成功执行 CAS ( _InterlockedCompareExchange64())。现在head 指向 B,它被释放(坏)并且 D 丢失(坏)。

这是经典的ABA problem。您应该使用第二个单词作为 generation number 而不是项目计数,即永远不要减少它。

现在有一个邮件列表discussion 正在进行关于实验性boost::lockfree 库。

另请查看Herb Sutter's lock-free queue - 这是一种不同的方法,其中一个虚拟节点可以防止生产者和消费者相互踩踏。

【讨论】:

  • 好的。那是个很好的观点。我假设我可以通过仅计数 16 位然后在每次弹出时在高 16 位中增加一个数字来轻松解决这个问题?这样,如果在另一个线程正在旋转时添加了 64K 项,我只会遇到问题,这似乎不太可能......
  • 是的,但你只是在降低概率:)
  • 要点...顺便说一句,我已经实施了herb sutter的方法。我发现基于临界区的系统表现不佳...我不太满意,特别是考虑到我通常对 Sutter 先生印象深刻...
  • 看看微软的 SList .. 他们似乎在做我上面建议的事情......我应该适当地对他们的代码进行反向工程,看看他们做了什么......
【解决方案3】:

正如您所发现的,无锁编程很难做到正确。

Windows 已经支持无锁单链表,http://msdn.microsoft.com/en-us/library/ms684121(VS.85).aspx,您应该尝试使用它而不是自己动手。

【讨论】:

  • 不,我很清楚。但我这样做的原因是我可以轻松地为其他平台重新实现......它更多的是学习练习:)
【解决方案4】:

您没有同步对列表标题成员的访问。这至少在两个层面上是不好的:

  • 给列表头赋值可能不像你想象的那么简单。这意味着不同步的读取操作可能会获得损坏的值。

  • 另一个更可能的问题是,如果您的机器有多个内核,则每个内核都可以在处理器缓存中拥有自己的值副本。要使它们同步您需要内存屏障的值

【讨论】:

  • InterlockedCompareExchange64 提供了内存屏障。
  • 这比看起来更糟糕 - 内存屏障是一项昂贵的操作,对于具有 n 个元素的列表,此实现将有 O(n) 个内存屏障
  • 通常没有逃逸的内存屏障。使用锁的实现,即使在没有争用的情况下,仍然会有内存屏障(如果锁不提供屏障,它们将毫无用处。)
  • 当然 - 但你不必拥有这么多。我猜每次通话一个就足够了
  • 理想情况下,每次调用一次。仅当头节点在获取数据之后但在交换之前发生更改时才进行重试。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-28
相关资源
最近更新 更多