2017-01-25 1 views
4

В пункте 16: «Сделать константные функции-члены поточно» есть код следующим образом:Эффективное размещение lock_guard - из пункта 16 из современных эффективных C++

class Widget { 
public:  
    int magicValue() const 
    { 
    std::lock_guard<std::mutex> guard(m); // lock m  
    if (cacheValid) return cachedValue; 
    else { 
     auto val1 = expensiveComputation1(); 
     auto val2 = expensiveComputation2(); 
     cachedValue = val1 + val2; 
     cacheValid = true; 
     return cachedValue; 
    } 
    }          // unlock m  
private: 
    mutable std::mutex m; 
    mutable int cachedValue;     // no longer atomic 
    mutable bool cacheValid{ false };  // no longer atomic 
}; 

Интересно, почему станд :: lock_guard должен быть выполняется всегда на каждом magicValue() вызова, Wouldnt следующие работы, как ожидалось ?:

class Widget { 
public: 

    int magicValue() const 
    { 

    if (cacheValid) return cachedValue; 
    else { 
     std::lock_guard<std::mutex> guard(m); // lock m 
     if (cacheValid) return cachedValue;   
     auto val1 = expensiveComputation1(); 
     auto val2 = expensiveComputation2(); 
     cachedValue = val1 + val2; 
     cacheValid = true; 
     return cachedValue; 
    } 
    }          // unlock m 

private: 
    mutable std::atomic<bool> cacheValid{false}; 
    mutable std::mutex m; 
    mutable int cachedValue;     // no longer atomic 
}; 

Этот способ меньше взаимных блокировок замков потребуется, что делает код более эффективным. Я предполагаю, что атомика всегда быстрее мьютексов.

[править]

Для Комплектность я измерил эффективность обоих apraches, а второй выглядит как только 6% быстрее .: http://coliru.stacked-crooked.com/a/e8ce9c3cfd3a4019

+2

Поскольку чтение cacheValid не может быть атомарным? –

+0

@NeilButterworth: добро пожаловать назад. –

+0

Вы понимаете, что ваши вычисления могут запускаться более одного раза со второй версией – steve

ответ

3

Ваш второй фрагмент кода показывает вполне правильная реализация перепроверили Блокировка Pattern (DCLP) и (возможно) более эффективным, что решение Мейерс, поскольку это позволяет избежать фиксирования mutex излишне после cachedValue установлен.

Гарантируется, что дорогостоящие вычисления выполняются не один раз.

Кроме того, важно, чтобы cacheValid флаг atomic, потому что это создает происходит, прежде, чем отношения между письменной и к чтению, от cachedValue. Другими словами, он синхронизирует cachedValue (к которому обращаются за пределами mutex) с другими темами, называющимися magicValue(). Если бы cacheValid был обычным «bool», у вас была бы гонка данных на обоих cacheValid и cachedValue (вызывая неопределенное поведение в стандарте C++ 11).

Использование последовательного последовательного последовательного упорядочения памяти на операциях памяти cacheValid прекрасно, поскольку подразумевает семантику получения/выпуска. В теории, можно оптимизировать с помощью слабых упорядоченности памяти на atomic нагрузок и магазина:

int Widget::magicValue() const 
{ 

    if (cacheValid.load(std::memory_order_acquire)) return cachedValue; 
    else { 
    std::lock_guard<std::mutex> guard(m); // lock m 
    if (cacheValid.load(std::memory_order_relaxed)) return cachedValue; 
    auto val1 = expensiveComputation1(); 
    auto val2 = expensiveComputation2(); 
    cachedValue = val1 + val2; 
    cacheValid.store(true, std::memory_order_release); 
    return cachedValue; 
    } 
} 

Заметим, что это лишь небольшая оптимизация, так как чтение atomic является регулярной нагрузки на многих платформах (что делает его эффективнее как чтение из неатома).

Как указал Нир Фридман, это работает только в одном направлении; вы не можете аннулировать cacheValid и перезапустить вычисления. Но это не было частью примера Мейерса.

2

Я на самом деле думаю, что ваш фрагмент кода правильно в изоляции, но опирается на предположение, которое обычно неверно в примере реального мира: оно предполагает, что cacheValid переходит от ложного к истинному, но никогда не может сделать обратную прогрессию, что становится недействительным.

В старом коде, mutex защищает все читает и пишет на cachedValue. В вашем новом коде есть доступ к чтению cachedValue вне мьютекса. Это означает, что один поток может прочитать это значение, в то время как другой поток пишет его. Уловка заключается в том, что чтение вне мьютекса произойдет только в том случае, если верно cacheValid. Но если верно cacheValid, письма не будет; cacheValid может стать реальностью после все записи завершены (обратите внимание, что это принудительно, так как оператор присваивания на cacheValid будет использовать строжайшую гарантию порядка памяти, поэтому его нельзя переупорядочить с помощью более ранних инструкций в блоке).

Но предположим, что написан какой-либо другой фрагмент кода, который может аннулировать кеш: Widget::invalidateCache(). Этот фрагмент кода ничего не делает, кроме как установить cacheValid на false. В старом коде, если вы повторно вызвали invalidateCache и magicValue из разных потоков, последняя функция может пересчитать значение или не в любой заданной точке. Но даже если ваши сложные вычисления возвращают разные значения каждый раз, когда они вызывают (потому что они используют глобальное состояние, скажем), вы всегда будете получать либо старое, либо новое значение, и больше ничего. Но теперь рассмотрим следующий порядок выполнения в коде:

  1. Thread 1 вызывает magicValue, и проверяет значение cacheValid. Это правда. Он прерывается, прежде чем он сможет продолжить.
  2. Тема 2 вызывает invalidateCache, а затем сразу же вызывает magicValue. magicValue видит, что кеш недействителен, получает мьютекс и начинает вычислять, а начинается с, написав cacheValid.
  3. Тема 1 прерывает чтение частично написанного cacheValid.

На самом деле, я не думаю, что этот пример работает на большинстве современных компьютеров, потому что int обычно будет 32 бита, и, как правило, 32-разрядные записи и чтения будут атомарными. Таким образом, на самом деле невозможно развернуть или «разорвать» значение cachedValue.Но на разных архитектурах, или если вы используете тип, отличный от целого (например, более чем на 64 бита), запись или чтение не гарантируется атомарным. Таким образом, вы можете получить в качестве возврата для magicValue то, что не является ни старым значением, ни новым значением, а некоторым странным поразрядным гибридом, который даже не является допустимым объектом.

Итак, хорошо, что вы нашли это. Я предполагаю, что, пытаясь свести пример к простоте, автор забыл, что больше не нужно быть строгим в том, чтобы поместить мьютекс снаружи.

+0

, вы правы, что на большинстве платформ нет целых чисел для чтения или записи. Но еще одна проблема с установкой недопустимого флага - это то, что cachedValue (неатомный) теперь может одновременно считываться и записываться. Это гонка данных, которая имеет неопределенное поведение – LWimsey

Смежные вопросы