我的Double-Checked Locking Pattern实现是否合适?

pre*_*uin 6 c++ multithreading mutex atomic double-checked-locking

Meyers的书" Effective Modern C++ "第16项中的一个例子.

在一个缓存昂贵的计算int的类中,您可能会尝试使用一对std :: atomic avriable而不是互斥锁:

class Widget {
public:
    int magicValue() const {
        if (cachedValid) {
            return cachedValue;
        } else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();

            cachedValue = va1 + val2;
            cacheValid = true;
            return cachedValue;
        }
    }
private:
    mutable std::atomic<bool> cacheValid { false };
    mutable std::atomic<int> cachedValue;
};
Run Code Online (Sandbox Code Playgroud)

这样可以工作,但有时它会比它应该工作得多.考虑:一个线程调用Widget :: magicValue,将cacheValid视为false,执行两个昂贵的计算,并将它们的总和分配给cachedValud.此时,第二个线程calidget Widget :: magicValue也将cacheValid视为false,因此执行与第一个线程刚刚完成的相同的昂贵计算.

然后他用互斥量给出了一个解决方案:

class Widget {
public:
    int magicValue() const {
        std::lock_guard<std::mutex> guard(m);
        if (cacheValid) {
            return cachedValue;
        } else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();

            cachedValue = va1 + val2;
            cacheValid = true;
            return cachedValue;
        }
    }
private:
    mutable std::mutex m;
    mutable bool cacheValid { false };
    mutable int cachedValue;
};
Run Code Online (Sandbox Code Playgroud)

但我认为解决方案不是那么有效,我考虑将互斥和原子结合起来组成一个双重检查锁定模式,如下所示.

class Widget {
public:
    int magicValue() const {
        if (!cacheValid)  {
            std::lock_guard<std::mutex> guard(m);
            if (!cacheValid) {
                auto val1 = expensiveComputation1();
                auto val2 = expensiveComputation2();

                cachedValue = va1 + val2;
                cacheValid = true;
            }
        }
        return cachedValue;
    }
private:
    mutable std::mutex m;
    mutable std::atomic<bool> cacheValid { false };
    mutable std::atomic<int> cachedValue;
};
Run Code Online (Sandbox Code Playgroud)

因为我是多线程编程的新手,所以我想知道:

  • 我的代码是对的吗?
  • 它的表现更好吗?

编辑:


修正了代码.if(!cachedValue) - > if(!cacheValid)

Tsy*_*rev 1

我的代码对吗?

是的。您应用的双重检查锁定模式是正确的。但请参阅下面的一些改进。

它的性能更好吗?

与完全锁定的变体(您的帖子中的第二个)相比,它大多具有更好的性能,直到magicValue()仅被调用一次(但即使在这种情况下,性能损失也可以忽略不计)。

与无锁变体(您的文章中的第一个)相比,您的代码显示出更好的性能,直到值计算比等待 mutex更快。

例如,10 个值的总和(通常)比等待 mutex更快。在这种情况下,第一个变体是优选的。从另一边来看,从文件读取 10 次比等待mutex慢,所以你的变体比第一个更好。


实际上,您的代码有一些简单的改进,可以使其更快(至少在某些机器上)并提高代码的理解:

  1. cachedValue变量根本不需要原子语义。它受cacheValid标志保护,原子性完成所有工作。此外,单个原子标志可以保护多个非原子值。

  2. 另外,正如该答案/sf/answers/2103496251/中所述,当访问cacheValid标志时,您不需要顺序一致性顺序(当您仅读取或写入原子变量时默认应用该顺序),释放- 获得订单就足够了。


class Widget {
public:
    int magicValue() const {
        //'Acquire' semantic when read flag.
        if (!cacheValid.load(std::memory_order_acquire))  { 
            std::lock_guard<std::mutex> guard(m);
            // Reading flag under mutex locked doesn't require any memory order.
            if (!cacheValid.load(std::memory_order_relaxed)) {
                auto val1 = expensiveComputation1();
                auto val2 = expensiveComputation2();

                cachedValue = va1 + val2;
                // 'Release' semantic when write flag
                cacheValid.store(true, std::memory_order_release);
            }
        }
        return cachedValue;
    }
private:
    mutable std::mutex m;
    mutable std::atomic<bool> cacheValid { false };
    mutable int cachedValue; // Atomic isn't needed here.
};
Run Code Online (Sandbox Code Playgroud)