Предположите, что ваш код будет запущен как часть многопоточной программы
Причина
Трудно быть уверенным, что параллелизм не используется сейчас и не будет использоваться когда-либо в будущем. Код переиспользуется. Библиотеки, не использующие потоки, могут быть использованы из какой-то другой части программы, которая использует потоки. Отметьте, что это правило наиболее срочно применяется к коду библиотеки и наименее срочно к автономным приложениям. Однако со временем фрагменты кода могут оказаться в неожиданных местах.
Плохой пример
double cached_computation(int x)
{
// bad: these statics cause data races in multi-threaded usage
static int cached_x = 0.0;
static double cached_result = COMPUTATION_OF_ZERO;
if (cached_x != x) {
cached_x = x;
cached_result = computation(x);
}
return cached_result;
}
Альтернативное Although cached_computation works perfectly in a single-threaded environment, in a multi-threaded environment the two static variables result in data races and thus undefined behavior.
Хороший пример
struct ComputationCache {
int cached_x = 0;
double cached_result = COMPUTATION_OF_ZERO;
double compute(int x) {
if (cached_x != x) {
cached_x = x;
cached_result = computation(x);
}
return cached_result;
}
};
Здесь кеш хранится как данные члена объекта ComputationCache, а не как общее статическое состояние. Эта рефакторизация по сути делегирует задачу вверх вызывающему: однопоточная программа может всё ещё выбрать один глобальный ComputationCache, в то время как многопоточная программа может иметь один экземпляр ComputationCache на поток, или один на "контекст" для любого определения "контекста". Перерефакторизованная функция больше не пытается управлять распределением cached_x. В этом смысле это применение принципа единственной ответственности.
В этом конкретном примере рефакторизация для потокобезопасности также улучшила переиспользуемость в однопоточных программах. Нетрудно представить, что однопоточная программа может захотеть два экземпляра ComputationCache для использования в разных частях программы, без того чтобы они перезаписывали друг другу кешированные данные.
Есть несколько других способов, которыми можно добавить потокобезопасность к коду, написанному для стандартной многопоточной среды (то есть такой, где единственной формой параллелизма является std::thread):
- Отметьте переменные состояния как
thread_localвместоstatic. - Реализуйте управление параллелизмом, например, защищая доступ к двум
staticпеременным с помощьюstatic std::mutex. - Откажитесь строить и/или запускать в многопоточной среде.
- Предоставьте две реализации: одну для однопоточных сред и другую для многопоточных сред.
Исключение
Код, который никогда не запускается в многопоточной среде.
Будьте осторожны: есть много примеров, где код, который был "известен" как никогда не запускаемый в многопоточной программе, был запущен как часть многопоточной программы, часто много лет спустя. Обычно такие программы приводят к болезненному усилию удалить состояния гонки. Поэтому код, который никогда не предназначен для запуска в многопоточной среде, должен быть ясно помечен как таковой и в идеале поступить с механизмами компиляции или времени выполнения для раннего захвата этих ошибок использования.