簡體   English   中英

我的Double-Checked Locking Pattern實現是否合適?

[英]Is my Double-Checked Locking Pattern implementation right?

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

這樣可以工作,但有時它會比它應該工作得多。考慮:一個線程調用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;
};

但我認為解決方案不是那么有效,我認為將mutex和atomic結合起來組成一個Double-Checked鎖定模式 ,如下所示。

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) - > if(!cacheValid)

正如HappyCactus所指出的,第二次檢查if (!cachedValue)實際上應該是if (!cachedValid) 除了這個錯字,我認為你的雙重鎖定模式的演示是正確的。 但是,我認為沒有必要在cachedValue上使用std::atomic 寫入cachedValue的唯一地方是cachedValue = va1 + val2; 在它完成之前,任何線程都不會到達語句return cachedValue; 這是唯一一個讀取cachedValue的地方。 因此,寫入和讀取不可能是並發的。 並發讀取沒有問題。

您可以通過減少內存排序要求來提高解決方案的效率。 此處不需要原子操作的默認順序一致性內存順序。

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

我的代碼是對的嗎?

是。 您應用雙重鎖定模式是正確的。 但請參閱下面的一些改進。

它的表現更好嗎?

與完全鎖定的變體(在你的帖子中為第2個)相比,它通常具有更好的性能,直到magicValue()僅被調用一次(但即使在那種情況下,性能損失也可以忽略不計)。

與無鎖變體(你的帖子中的第一個)相比,你的代碼表現出更好的性能,直到值計算比等待互斥鎖更快。

例如, 10個值的總和 (通常)比等待互斥鎖的 速度快 在那種情況下,第一種變體是可取的。 另一方面, 10個文件讀取等待互斥鎖 ,所以你的變體比1st更好。


實際上,您的代碼有一些簡單的改進,這使得它更快(至少在某些機器上)並提高代碼的理解:

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

這是不正確的:

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;

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM