【问题标题】:Lockless reader/writer无锁读写器
【发布时间】:2010-07-29 05:53:20
【问题描述】:

我有一些数据由多个线程读取和更新。读取和写入都必须是原子的。我正在考虑这样做:

// Values must be read and updated atomically
struct SValues
{
    double a;
    double b;
    double c;
    double d;
};

class Test
{
public:
    Test()
    {
        m_pValues = &m_values;
    }

    SValues* LockAndGet()
    {
        // Spin forver until we got ownership of the pointer
        while (true)
        {
            SValues* pValues = (SValues*)::InterlockedExchange((long*)m_pValues, 0xffffffff);
            if (pValues != (SValues*)0xffffffff)
            {
                return pValues;
            }
        }
    }

    void Unlock(SValues* pValues)
    {
        // Return the pointer so other threads can lock it
        ::InterlockedExchange((long*)m_pValues, (long)pValues);
    }

private:
    SValues* m_pValues;
    SValues m_values;
};

void TestFunc()
{
    Test test;

    SValues* pValues = test.LockAndGet();

    // Update or read values

    test.Unlock(pValues);
}

通过在每次读写时窃取指向它的指针来保护数据,这应该使其成为线程安全的,但每次访问都需要两条互锁指令。将会有大量的读取和写入,我无法提前知道是否会有更多的读取或更多的写入。

可以做得比这更有效吗?这在读取时也会锁定,但由于很可能有更多的写入然后读取,因此优化读取没有意义,除非它不会对写入造成惩罚。

我正在考虑在没有互锁指令(以及序列号)的情况下读取指针,复制数据,然后有一种方法来判断序列号是否已更改,在这种情况下它应该重试。不过,这需要一些内存屏障,我不知道它是否可以提高速度。

----- 编辑-----

谢谢大家,伟大的 cmets!我实际上并没有运行此代码,但我会在今天晚些时候尝试将当前方法与关键部分进行比较(如果我有时间的话)。我仍在寻找最佳解决方案,因此稍后我将回到更高级的 cmets。再次感谢!

【问题讨论】:

  • 使用默认线程同步原语有什么问题?
  • 我必须承认我只是假设我可以做得比这更快。 1) 我在这里只显示一个实例,但实际上我可能会有 10000 个这些受保护数据记录的实例,这意味着 10000 个关键部分。但也许这不是问题,我不知道,我从未尝试过这样的事情。 2)我希望我能比关键部分更快地想出一些东西。每秒很容易有数百万次读取/写入。在个人层面上,我只是认为尽可能快地以人工方式(机器?)让它变得有趣。
  • Windows CRITICAL_SECTION 非常轻量级,除非它实际上必须阻塞。我不认为像这样忙着等待的用户线程是一个非常好的主意——你隐含地向调度程序发出信号说你有很多事情要做,而实际上情况恰恰相反。
  • 在 windows 上:MemoryBarrier 被测量为需要 20-90 个周期。 InterlockedIncrement 被测量为需要 36-90 个周期。获取或释放关键部分被测量为需要 40-100 个周期。测量获得或释放互斥体大约需要 750-2500 个周期。

标签: c++ lockless


【解决方案1】:

你所写的本质上是一个自旋锁。如果您打算这样做,那么您不妨只使用一个互斥体,例如boost::mutex。如果您真的想要一个自旋锁,请使用系统提供的自旋锁,或者使用库中的自旋锁,而不是自己编写。

其他可能性包括执行某种形式的写时复制。通过指针存储数据结构,并在读取端(原子地)读取指针。然后在写入端创建一个新实例(根据需要复制旧数据)并原子交换指针。如果写入确实需要旧值并且有多个写入器,那么您将需要执行比较交换循环以确保该值在您读取后没有更改(注意 ABA 问题),或者为作家们。如果您这样做,那么您需要小心管理内存的方式 --- 当没有线程引用数据时(但不是之前),您需要一些方法来回收数据实例。

【讨论】:

  • 没错,它是一个自旋锁。经过一番研究,似乎可以实现自旋锁,而无需在退出时使用锁定指令,并且在自旋时无需编写任何内容。这可能是我正在寻找的解决方案。我现在很忙,但我稍后会回来。
  • 到目前为止,我最好的解决方案是为每个需要保护的指针使用自旋锁。它们每个只使用 4 个字节。它与这个问题中的示例代码基本相同,除了自旋锁在旋转时不会使用互锁指令,而是使用 asm 暂停指令。
【解决方案2】:

有几种方法可以解决这个问题,特别是不使用互斥锁或锁定机制。问题是我不确定你的系统有什么限制。

请记住,原子操作是 C++ 中的编译器经常移动的东西。

一般我会这样解决问题:

多生产者-单消费者,每个写入线程有 1 个单生产者-单消费者。每个线程写入自己的队列。单个消费者线程,它收集生成的数据并将其存储在单消费者多阅读器数据存储中。实现这一点需要做很多工作,只有在您正在做一个时间要求严格的应用程序并且您有时间为这个解决方案投入使用时,才建议您这样做。

还有更多内容需要阅读,因为实现是特定于平台的:

windows/xbox360 上的原子等操作: http://msdn.microsoft.com/en-us/library/ee418650(VS.85).aspx

不带锁的多线程单生产者单消费者:
http://www.codeproject.com/KB/threads/LockFree.aspx#heading0005

什么是“易失性”并且可以用于:
http://www.drdobbs.com/cpp/212701484

Herb Sutter 写了一篇很好的文章,提醒您编写这种代码的危险: http://www.drdobbs.com/cpp/210600279;jsessionid=ZSUN3G3VXJM0BQE1GHRSKHWATMY32JVN?pgno=2

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-25
  • 2011-12-04
  • 2015-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多