Приведенный ниже код используется для назначения работы нескольким потокам, их пробуждения и ожидания завершения. «Работа» в этом случае состоит из «очистки объема». Что именно делает эта операция, не имеет значения для этого вопроса — она просто помогает в контексте. Код является частью огромной системы обработки транзакций.
void bf_tree_cleaner::force_all()
{
for (int i = 0; i < vol_m::MAX_VOLS; i++) {
_requested_volumes[i] = true;
}
// fence here (seq_cst)
wakeup_cleaners();
while (true) {
usleep(10000); // 10 ms
bool remains = false;
for (int vol = 0; vol < vol_m::MAX_VOLS; ++vol) {
// fence here (seq_cst)
if (_requested_volumes[vol]) {
remains = true;
break;
}
}
if (!remains) {
break;
}
}
}
Значение в логическом массиве _requested_volumes[i]
говорит ли нить i
есть работа, чтобы сделать. Когда это сделано, рабочий поток устанавливает значение false и возвращается в спящий режим.
У меня проблема в том, что компилятор генерирует бесконечный цикл, где переменная remains
всегда true, хотя все значения в массиве были установлены в false. Это происходит только с -O3
,
Я пробовал два решения, чтобы исправить это:
_requested_volumes
летучийМногие эксперты утверждают, что volatile не имеет ничего общего с синхронизацией потоков и должно использоваться только для низкоуровневого доступа к оборудованию. Но в Интернете много споров по этому поводу. Насколько я понимаю, volatile — единственный способ удержать компилятор от оптимизации доступа к памяти, которая изменяется вне текущей области видимости, независимо от одновременного доступа. В этом смысле изменчив должен добейтесь успеха, даже если мы не согласны с рекомендациями по параллельному программированию.
Метод wakeup_cleaners()
приобретает pthread_mutex_t
внутренне для того, чтобы установить флаг пробуждения в рабочих потоках, чтобы он неявно создавал надлежащие ограждения памяти. Но я не уверен, влияют ли эти заборы на доступ к памяти в методе вызывающей стороны (force_all()
). Поэтому я вручную ввел заборы в местах, указанных в комментариях выше. Это должно убедиться, что записи выполняются рабочим потоком в _requested_volumes
видны в основной теме.
Меня озадачивает то, что ни одно из этих решений не работает, и я абсолютно не знаю, почему. Семантика и правильное использование ограждений и изменчивости памяти сбивают меня с толку прямо сейчас. Проблема в том, что компилятор применяет нежелательную оптимизацию — отсюда и непостоянная попытка. Но это также может быть проблемой синхронизации потоков — отсюда и попытка ограничения памяти.
Я мог бы попробовать третье решение, в котором мьютекс защищает каждый доступ к _requested_volumes
Но даже если это сработает, я хотел бы понять, почему, потому что, насколько я понимаю, все дело в заборах памяти. Таким образом, не должно иметь значения, делается ли это явно или неявно через мьютекс.
РЕДАКТИРОВАТЬ: Мои предположения были неверны, и решение 1 на самом деле делает Работа. Тем не менее, мой вопрос остается для того, чтобы уточнить использование энергозависимых и памяти заборов. Если volatile — такая плохая вещь, которая никогда не должна использоваться в многопоточном программировании, что еще я должен использовать здесь? Влияет ли ограничение памяти на оптимизацию компилятора? Потому что я рассматриваю их как две ортогональные проблемы и, следовательно, ортогональные решения: ограждения для видимости в нескольких потоках и изменчивые для предотвращения оптимизации.
Многие эксперты утверждают, что volatile не имеет ничего общего с синхронизацией потоков и должно использоваться только для низкоуровневого доступа к оборудованию.
Да.
Но в Интернете много споров по этому поводу.
Не, вообще, между «экспертами».
Как я понимаю, volatile — единственный способ удержать компилятор от оптимизации доступа к памяти, которая изменяется вне текущей области, независимо от одновременного доступа.
Нету.
Неполные, не встроенные вызовы функций non-constexpr (getters / accessors) также обязательно имеют этот эффект. По общему признанию, оптимизация времени соединения путает вопрос о том, какие функции действительно могут быть встроены.
В C и, как следствие, C ++, volatile
влияет на оптимизацию доступа к памяти. Java взяла это ключевое слово, и так как она не может (или не может) выполнять задачи, которые использует C volatile
во-первых, изменил его, чтобы обеспечить забор памяти.
Правильный способ получить тот же эффект в C ++ использует std::atomic
,
В этом смысле volatile должна сработать, даже если мы не согласны с рекомендациями по параллельному программированию.
Нет это может иметь желаемый эффект, в зависимости от того, как он взаимодействует с оборудованием кэша вашей платформы. Это хрупко — оно может измениться в любое время, когда вы обновляете ЦП, или добавляете другой, или изменяете свое поведение планировщика — и это, конечно, не переносимо.
Если вы действительно просто отслеживаете Как много рабочие все еще работают, вменяемые методы могут быть семафором (синхронизированным счетчиком) или мьютексом + condvar + целым числом. Любой из них, вероятно, более эффективен, чем зацикливание во сне.
Если вы подключены к циклу занятости, вы все равно можете иметь не замужем счетчик, такой как std::atomic<size_t>
, который устанавливается wakeup_cleaners
и уменьшается по мере завершения каждого уборщика. Тогда вы можете просто подождать, пока оно достигнет нуля.
Если вы действительно хотите занятой петли а также действительно предпочитают сканировать массив каждый раз, это должен быть массив std::atomic<bool>
, Таким образом, вы можете решить, какая согласованность вам нужна от каждой загрузки, и она будет контролировать обе оптимизации компилятора. а также аппаратное обеспечение памяти соответственно.
По-видимому, volatile
делает необходимое для вашего примера. Тема о volatile
Сам по себе квалификатор слишком широк: вы можете начать с поискаC ++ изменчивый против атомарногои т. д. Есть много статей и вопросов&ответы в интернете, например Параллельность: атомарная и энергозависимая в модели памяти C ++ 11 .
Коротко говоря, volatile
говорит компилятору отключить некоторые агрессивные оптимизации, в частности, читать переменную при каждом обращении к ней (а не сохранять ее в регистре или кэше). Есть компиляторы, которые делают больше volatile
вести себя как std::atomic
: см. раздел Microsoft Specific Вот. В вашем случае отключение агрессивной оптимизации — это именно то, что было необходимо.
Тем не мение, volatile
не определяет порядок выполнения операторов вокруг него. Вот почему вам нужно порядок памяти в случае, если вам нужно сделать что-то еще с данными после того, как установленные вами флаги были установлены.
Для связи между потоками целесообразно использовать std::atomic
в частности, вам необходимо провести рефакторинг _requested_volumes[vol]
быть типом std::atomic<bool>
или даже std::atomic_flag
: http://en.cppreference.com/w/cpp/atomic/atomic .
Статья, которая препятствует использованию volatile и объясняет, что volatile можно использовать только в редких особых случаях (связанных с аппаратным вводом / выводом): https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt