Правильна ли моя реализация шаблона блокировки с двойной проверкой?

Пример в книге Мейерса Эффективный Современный 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;
};

Это будет работать, но иногда это будет работать намного тяжелее, чем это
should.Consider: поток вызывает Widget :: magicValue, видит cacheValid как
false, выполняет два дорогих вычисления и присваивает их сумму
к кешированию На этом этапе второй поток calss
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) -> if (! cacheValid)

6

Решение

Правильный ли мой код?

Да. Вы правильно применили шаблон блокировки с двойной проверкой. Но смотрите ниже для некоторых улучшений.

Это производительность лучше?

По сравнению с полностью заблокированным вариантом (2-й в вашем посте), он в основном имеет лучшую производительность, пока magicValue() вызывается только один раз (но даже в этом случае потери производительности пренебрежимо малы).

По сравнению с вариантом без блокировки (1-й в вашем посте) ваш код показывает лучшую производительность, пока вычисление значений не будет быстрее, чем в ожидании мьютекса.

Например., сумма 10 значений обычно) Быстрее чем в ожидании мьютекса. В этом случае 1-й вариант предпочтительнее. С другой стороны, 10 чтений из файла является помедленнее чем в ожидании мьютекса, так что твой вариант лучше первого.


На самом деле, существуют простые улучшения вашего кода, которые ускоряют его (по крайней мере, на некоторых машинах) и улучшают понимание кода:

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

Другие решения

Как отмечает HappyCactus, вторая проверка if (!cachedValue) должно быть на самом деле if (!cachedValid), За исключением этой опечатки, я думаю, что ваша демонстрация шаблона блокировки с двойной проверкой верна. Тем не менее, я думаю, что нет необходимости использовать std::atomic на cachedValue, Единственное место, где cachedValue написано в cachedValue = va1 + val2;, До его завершения ни один поток не достигнет оператора return cachedValue; который является единственным местом cachedValue читается Следовательно, запись и чтение не могут быть параллельными. И нет проблем с одновременным чтением.

2

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

Разница в производительности может быть незначительной на x86, но заметна на ARM, потому что последовательный порядок памяти последовательности дорог на ARM. Увидеть «Сильные» и «слабые» аппаратные модели памяти от Херба Саттера Больше подробностей.

Предлагаемые изменения:

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

Это не правильно

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;
-1
По вопросам рекламы [email protected]