多线程环境中的延迟加载数据

时间:2013-12-29 16:07:18

标签: c++ multithreading mutex atomic

我有这样的结构:

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() {}
};
mSubLevels中的

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

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

现在我可以使用常规数据类型,还是必须使用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(正如我已经写过)并使用'acquire'来加载第二个函数:

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 ...

Reader线程将看到来自编写器线程的更改,并且不会发生cache coherence问题。这句话是真的吗?

2 个答案:

答案 0 :(得分:3)

你的问题远远超过缓存一致性。这是关于正确性的。你正在做的是double checked locking的案例。

只要一个线程看到mSubLevels为空并分配新对象,就会出现问题。在发生这种情况时,另一个线程可以同时访问mSubLevels并看到它为null,并分配一个对象。现在怎么办?哪一个是分配给指针的“正确”对象。你会泄漏一个物体,或者你用另一个物体做什么?如何检测这种状况?

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

所以总之,是的,你必须至少使用原子操作,甚至是互斥。使用“普通”数据类型将无效。

对于其他只有读者而没有作家的人来说,你可以使用常规类型,它可以正常工作。

答案 1 :(得分:1)

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

  1. 如果没有创建阵列,你无法承受阅读,显然
  2. 出于效率原因,您可能不希望多次创建数组
  3. 我建议只使用读写器互斥锁

    基本理念是:

    • 锁定阅读器模式
    • 检查数据是否准备就绪
    • 如果没有准备好,请将锁升级为编写模式
    • 检查数据是否准备好(可能是由另一位作者准备好的),如果没有准备好
    • 在编写器模式下释放锁定(保持锁定在读卡器模式下)
    • 使用数据做事
    • 以读取器模式释放锁定

    此设计存在一些问题(特别是初始化期间发生的争用),但它具有简单易用的优点。