【问题标题】:Lazy loaded data in multithreaded environment多线程环境下延迟加载数据
【发布时间】:2014-01-16 14:31:34
【问题描述】:

我有一个这样的结构:

struct Chunk
{
private:

public:
    Chunk* mParent;
    Chunk* mSubLevels;
    Int16 mDepth;
    Int16 mIndex;
    Reference<ValueType> mFirstItem;
    Reference<ValueType> mLastItem;

public:
    Chunk()
    {
        mSubLevels = nullptr;
        mFirstItem = nullptr;
        mLastItem = nullptr;
    }
    ~Chunk() {}
};

chunk 中的mSubLevels 在首次访问之前为空。在第一次访问mSubLevels 时,我为mSubLevels 创建一个chunks 数组并填充其他成员。但是因为多个线程与chunks 一起工作,所以我使用mutex 执行此过程。所以创建新的chunks 受到mutex 的保护。在这个过程之后没有写入这个chunks 并且它们是只读数据,所以线程访问这个chunks 而没有任何mutex

确实,我有一些方法,在其中一个方法中,在第一次访问mSubLevels 时,我检查这个指针,如果它为空,我将通过mutex 创建所需的数据。但其他方法是只读的,我不会更改structure。所以我在这个函数中不使用任何mutex。 (在创建chunks 的线程和读取它们的线程之间没有任何acquire/release 排序)。

现在我可以使用常规数据类型,还是必须使用atomic 类型?

编辑 2:

为了创建数据我使用double checked locking:

(这是一个将创建新的chunks的函数)

Chunk* lTargetChunk = ...;
if (!std::atomic_load(lTargetChunk->mSubLevels, std::memory_order_relaxed))
{
    std::lock_guard lGaurd(mMutex);

    if (!std::atomic_load(lTargetChunk->mSubLevels, std::memory_order_relaxed))
    {
        Chunk* lChunks = new Chunk[mLevelSizes[l]];
        for (UINT32 i = 0; i < mLevelSizes[l]; ++i)
        {
            Chunk* lCurrentChunk = &lChunks[i];
            lCurrentChunk->mParent = lTargetChunk;
            lCurrentChunk->mDepth = lDepth - 1;
            lCurrentChunk->mIndex = i;
            st::atomic_store(lCurrentChunk->mSubLevels, (Chunk*)bcNULL, memory_order_relaxed);
        }
        bcAtomicOperation::bcAtomicStore(lTargetChunk->mSubLevels, lChunks, std::memory_order_release);
    }

}

暂时,想象一下我对mSubLevels 不使用原子操作。

我还有一些其他方法只能读取这个chunks 而没有任何“互斥锁”:

bcInline Chunk* _getSuccessorChunk(const Chunk* pChunk)
{
    // If pChunk->mSubLevels isn't null do this operation.
    const Chunk* lChunk = &pChunk->mSubLevels[0];
    Chunk* lNextChunk;

    if (lChunk->mIndex != mLevelSizes[lChunk->mDepth] - 1)
    {
        lNextChunk = lChunk + 1;
        return lNextChunk;
    }
    else ...

如您所见,我可以访问mSubLevelsmIndex 和其他一些。在此函数中,我不使用任何“互斥锁”,因此如果编写器线程不将其缓存刷新到主内存,则将运行此函数的任何线程都不会看到受影响的更改。如果我在这个函数中使用mMutex,我认为问题将得到解决。 (写入器线程和读取器线程将通过互斥锁中的原子操作同步)现在,如果我在第一个函数中使用 mSubLevels 的原子操作(如我所写)并使用“获取”在第二个函数中加载它:

bcInline Chunk* _getSuccessorChunk(const Chunk* pChunk)
{
    // If pChunk->mSubLevels isn't null do this operation.
    const Chunk* lChunk = &std::atomic_load(pChunk->mSubLevels, std::memory_order_acquire)[0];
    Chunk* lNextChunk;

    if (lChunk->mIndex != mLevelSizes[lChunk->mDepth] - 1)
    {
        lNextChunk = lChunk + 1;
        return lNextChunk;
    }
    else ...

读取线程将看到写入线程的更改,不会发生cache coherence 问题。这句话是真的吗?

【问题讨论】:

  • 我的问题是cache coherency

标签: c++ multithreading mutex atomic


【解决方案1】:

您的问题不仅仅是缓存一致性问题。这是关于正确性的。你正在做的是double checked locking的案例。

这是有问题的,因为一个线程可能会看到mSubLevels 为空并分配一个新对象。当这种情况发生时,另一个线程可能同时访问mSubLevels 并看到它为空,并分配一个对象。现在怎么办?哪一个是要分配给指针的“正确”对象。您会只泄漏一个对象,或者您如何处理另一个对象?如何检测这种情况?

要解决这个问题,您必须在检查值之前锁定(即使用互斥锁),或者您必须执行某种原子操作来区分空对象和仍然无效的对象正在创建的对象和一个有效的对象(例如与(Chunk*)1 的原子比较交换,基本上类似于微自旋锁,除非您没有旋转)。

总之,是的,您至少必须为此使用原子操作,甚至是互斥体。使用“普通”数据类型是行不通的。

对于只有读者而没有作者的其他一切,你可以使用常规类型,它会工作得很好。

【讨论】:

  • 谢谢,我使用double check lock 来创建数据。
  • 我可以使用原子变量来同步数据以避免cache coherency像我的编辑这样的问题吗?
  • 是的,也不是。您想要的称为“内存排序”,而不是缓存一致性。您选择的顺序是您使用的正确选择,但是一个简单的 store 不足以避免可能创建两个对象的问题。为此,您要么需要一个互斥体(然后内存顺序无关紧要,因为互斥体已经是一个完整的障碍),要么您必须使用带有 compare-exchange 的构造(在这种情况下,显然也必须调整所有对 null 的检查)。
  • 措辞不同,您在编辑中发布的存储/加载将确保指针是原子设置的,并且它将保证您所做的所有操作对指针执行加载的任何线程都可以看到之前完成的操作。然而,它们本身并不能阻止双重创造。
  • 你能看到我的新编辑吗?请原谅我的问题,感谢您抽出宝贵时间。
【解决方案2】:

这里有两个问题你需要克服:

  1. 显然,如果不创建数组,您将无法阅读
  2. 出于效率原因,您可能不想多次创建数组

我建议只使用读写器互斥锁

基本思路是:

  • 锁定阅读器模式
  • 检查数据是否准备好
  • 如果没有准备好,将锁升级到写入器模式
  • 检查数据是否已准备好(可能已由其他编写者准备好),如果没有准备好
  • 在写入器模式下释放锁(在读取器模式下保持锁)
  • 用数据做事
  • 在阅读器模式下释放锁

这种设计存在一些问题(特别是在初始化期间发生的争用),但它的优点是非常简单。

【讨论】:

  • 你的意思是在每次访问结构时我都必须使用读卡器锁?在一种方法中,如果数组为空,我必须创建数组(在本节中,我使用dcl),但在其他方法中,如果数组为空,我不使用它并继续...
  • 我可以使用原子变量来同步数据以避免cache coherency像我的编辑这样的问题吗?
  • @MohammadRB:显然,如果您不需要该数组,则无需对其进行初始化。至于缓存一致性问题,即使没有原子变量,使用互斥锁也可以确保适当的一致性……但是,我突然在这里很不自在。多线程是一个复杂的话题,我们只是绕过基础知识,如果您之前从未使用过互斥锁、条件变量或信号量,那么我担心 StackOverflow 不适合您学习使用它们;你需要一个更有条理的课程。我推荐 Anthony Williams 的 C++ Concurrency In Action
  • 你能看到我的新编辑吗?请原谅我的问题,感谢您抽出宝贵时间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-02-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-06
相关资源
最近更新 更多