6

Meyers 的《Effective Modern C++ 》一书中的一个例子,第 16 条。

在缓存计算成本高的 int 的类中,您可能会尝试使用一对 std::atomic 可变量而不是互斥锁:

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;
};

这会起作用,但有时它会比应有的工作更难。考虑:一个线程调用 Widget::magicValue,将 cacheValid 视为 false,执行两个昂贵的计算,并将它们的总和分配给 cachedValud。此时,第二个线程调用 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;
};

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

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;
};

因为我是多线程编程的新手,所以想了解一下:</p>

  • 我的代码对吗?
  • 它的性能更好吗?

编辑:


修复了代码。if (!cachedValue) -> if (!cacheValid)

4

4 回答 4

2

正如 HappyCactus 所指出的,第二次检查if (!cachedValue)实际上应该是if (!cachedValid). 除了这个错字,我认为您对双重检查锁定模式的演示是正确的。但是,我认为没有必要使用std::atomicon cachedValue。唯一写入的地方cachedValuecachedValue = va1 + val2;. 在它完成之前,没有线程会到达return cachedValue;唯一cachedValue被读取的地方的语句。因此,写入和读取不可能同时进行。并且并发读取没有问题。

于 2015-05-05T09:43:35.233 回答
0

您可以通过降低内存排序要求来使您的解决方案更加高效。此处不需要原子操作的默认顺序一致性内存顺序。

性能差异在 x86 上可能可以忽略不计,但在 ARM 上很明显,因为顺序一致性内存顺序在 ARM 上很昂贵。有关详细信息,请参阅Herb Sutter的“强”和“弱”硬件内存模型。

建议更改:

class Widget {
public:
    int magicValue() const {
        if (cachedValid.load(std::memory_order_acquire)) { // Acquire semantics.
            return cachedValue;
        } else {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();

            cachedValue = va1 + val2; // Non-atomic write.

            // Release semantics.
            // Prevents compiler and CPU store reordering.
            // Makes this and preceding stores by this thread visible to other threads.
            cachedValid.store(true, std::memory_order_release); 
            return cachedValue;
        }
    }
private:
    mutable std::atomic<bool> cacheValid { false };
    mutable int cachedValue; // Non-atomic.
};
于 2015-05-05T10:04:01.043 回答
0

我的代码对吗?

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

它的性能更好吗?

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

与无锁变体(您的帖子中的第一个)相比,您的代码表现出更好的性能,直到价值计算比等待 mutex更快。

例如,10 个值的总和(通常)比等待 mutex。在这种情况下,第一个变体是可取的。另一方面,从文件读取 10 次比等待mutex,因此您的变体比第一次好。


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

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

  2. 此外,如该答案https://stackoverflow.com/a/30049946/3440745中所述,当访问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.
};
于 2015-05-05T11:32:03.303 回答
-1

这是不正确的:

int magicValue() const {
    if (!cachedValid)  {

        // this part is unprotected, what if a second thread evaluates
        // the previous test when this first is here? it behaves 
        // exactly like in the first example.

        std::lock_guard<std::mutex> guard(m);
        if (!cachedValue) {
            auto val1 = expensiveComputation1();
            auto val2 = expensiveComputation2();

            cachedValue = va1 + val2;
            cachedValid = true;
        }
    }
    return cachedValue;
于 2015-05-05T09:21:39.733 回答