我的Double-Checked锁定模式实现是否正确?

时间:2015-05-05 08:49:33

标签: c++ multithreading mutex atomic double-checked-locking

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

  

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

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

这样可行,但有时它会比它更难   should.Consider:一个线程调用Widget :: magicValue,将cacheValid视为   false,执行两个昂贵的计算,并分配它们的总和   to 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;
};

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

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

修改

修正了代码。 if(!cachedValue) - &gt; if(!cacheValid)

3 个答案:

答案 0 :(得分:0)

通过减少内存排序要求,您可以提高解决方案的效率。此处不需要原子操作的默认顺序一致性内存顺序。

在x86上性能差异可以忽略不计,但在ARM上却很明显,因为ARM上的顺序一致性内存顺序很昂贵。有关详细信息,请参阅“Strong” and “weak” hardware memory models by 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.
};

答案 1 :(得分:0)

  

我的代码是对的吗?

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

  

性能更好吗?

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

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

例如, 10个值的总和(通常)更快等待互斥。在那种情况下,第一种变体是可取的。从另一方面来说, 10从文件中读取等待互斥 更慢,所以你的变体比第1更好。

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

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

  2. 此外,正如答案https://stackoverflow.com/a/30049946/3440745中所述,当访问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. }; 标志时,您不需要顺序一致性顺序(当您只是读取或写入原子变量时默认应用顺序一致性顺序),释放 - 获取订单就足够了。

  3. /**
     *
     * @param startingDay - day of the week starting point ( need to be between 0-6 )
     * @param noDays number of days to count
     * @return result Day of the week
     */
    private static WeekDays getWeekDay(int startingDay, int noDays){
        int dayNr = noDays % 7;
        int finalDayNr = (startingDay + dayNr) % 7;
        return WeekDays.values()[finalDayNr];
    }
    
    private static enum WeekDays {
        SUNDAY,
        MONDAY,
        TUESDAY,
        WEDNESDAY,
        THURSDAY,
        FRIDAY,
        SATURDAY
    }
    

答案 2 :(得分:-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;