Летучие против памяти заборы

Приведенный ниже код используется для назначения работы нескольким потокам, их пробуждения и ожидания завершения. «Работа» в этом случае состоит из «очистки объема». Что именно делает эта операция, не имеет значения для этого вопроса — она ​​просто помогает в контексте. Код является частью огромной системы обработки транзакций.

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,

Я пробовал два решения, чтобы исправить это:

  1. декларировать _requested_volumes летучий
    (РЕДАКТИРОВАТЬ: это решение действительно работает на самом деле. См редактировать ниже)

Многие эксперты утверждают, что volatile не имеет ничего общего с синхронизацией потоков и должно использоваться только для низкоуровневого доступа к оборудованию. Но в Интернете много споров по этому поводу. Насколько я понимаю, volatile — единственный способ удержать компилятор от оптимизации доступа к памяти, которая изменяется вне текущей области видимости, независимо от одновременного доступа. В этом смысле изменчив должен добейтесь успеха, даже если мы не согласны с рекомендациями по параллельному программированию.

  1. Ввести заборы памяти

Метод wakeup_cleaners() приобретает pthread_mutex_t внутренне для того, чтобы установить флаг пробуждения в рабочих потоках, чтобы он неявно создавал надлежащие ограждения памяти. Но я не уверен, влияют ли эти заборы на доступ к памяти в методе вызывающей стороны (force_all()). Поэтому я вручную ввел заборы в местах, указанных в комментариях выше. Это должно убедиться, что записи выполняются рабочим потоком в _requested_volumes видны в основной теме.

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

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


РЕДАКТИРОВАТЬ: Мои предположения были неверны, и решение 1 на самом деле делает Работа. Тем не менее, мой вопрос остается для того, чтобы уточнить использование энергозависимых и памяти заборов. Если volatile — такая плохая вещь, которая никогда не должна использоваться в многопоточном программировании, что еще я должен использовать здесь? Влияет ли ограничение памяти на оптимизацию компилятора? Потому что я рассматриваю их как две ортогональные проблемы и, следовательно, ортогональные решения: ограждения для видимости в нескольких потоках и изменчивые для предотвращения оптимизации.

1

Решение

Многие эксперты утверждают, что 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>, Таким образом, вы можете решить, какая согласованность вам нужна от каждой загрузки, и она будет контролировать обе оптимизации компилятора. а также аппаратное обеспечение памяти соответственно.

4

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

По-видимому, 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

1

По вопросам рекламы [email protected]