Можно ли реализовать в Linux правильный отказоустойчивый общий барьер процессов?

В прошлом вопросе я спрашивал о реализации барьеров pthread без гонок уничтожения:

Как барьеры могут быть разрушены, как только pthread_barrier_wait возвращается?

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

Можно ли в Linux сделать барьер, отвечающий этим условиям:

  • Общий процесс (может быть создан в любой разделяемой памяти).
  • Безопасно отключать или уничтожать барьер из любого потока сразу после возврата функции ожидания барьера.
  • Не удается завершиться из-за сбоя выделения ресурсов.

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

Решение в пространстве ядра может быть единственным способом, но даже это сложно из-за возможности прерывания ожидания сигналом без надежного способа возобновить его...


person R.. GitHub STOP HELPING ICE    schedule 04.08.2011    source источник
comment
Я не уверен, что действительно понимаю ваш вопрос, насколько я могу судить, реализация NPTL отказоустойчива. Сам барьер размещен снаружи, его функция pthread_barrier_destroy синхронизируется только с последним ожидающим. в чем именно проблема?   -  person Hasturkun    schedule 04.08.2011
comment
@Hasturkun: см. ошибку glibc № 13065: sourceware.org/bugzilla/show_bug.cgi?id =13065   -  person R.. GitHub STOP HELPING ICE    schedule 24.09.2011
comment
Хотя вложение там отсутствует, похоже, оно описывает случай, когда память не отображается до того, как барьер будет разрушен. Если я правильно понимаю, никогда не бывает безопасно отключать барьер перед его уничтожением (если только вы не можете гарантировать, что нет ни одного потока, использующего барьер, если вы не последний пользователь). реализация NPTL никогда не обращается к чему-либо без блокировки, которую берет pthread_barrier_destroy, гарантируя, что никто не получит доступ к внутренним компонентам барьера, когда уничтожение будет успешным (чего не произойдет, если какой-либо поток ожидает).   -  person Hasturkun    schedule 25.09.2011
comment
Безопасно отменить сопоставление барьера до его уничтожения, пока остаются другие разрушаемые сопоставления (например, в других процессах). Это было бы стандартным использованием для объектов синхронизации общего процесса, когда одному процессу требуется доступ к ним только в течение фиксированного времени.   -  person R.. GitHub STOP HELPING ICE    schedule 25.09.2011
comment
Было бы безопасно, если бы вы могли гарантировать, что ваш процесс не использует его, и в этом случае действительно не должно быть проблем. Я не вижу причин ожидать какого-либо поведения, если вы решите освободить базовую память барьера (или любого другого примитива) до того, как соответствующие функции вернутся. Вы должны синхронизировать всех пользователей перед отключением памяти, либо явно, либо правильно используя pthread_barrier_destroy. В последнем случае вы можете позже повторно инициализировать барьер для повторного использования.   -  person Hasturkun    schedule 25.09.2011
comment
Кстати, я думаю, что реализация NPTL будет соответствовать требованиям unmap, если вы измените второе условие, чтобы все функции ожидания барьера возвращались. В настоящее время он обеспечивает безопасность только в том случае, если один поток (например, PTHREAD_BARRIER_SERIAL_THREAD) уничтожает, а затем разъединяет.   -  person Hasturkun    schedule 25.09.2011
comment
Вы читаете спецификацию так, как ленивый разработчик предпочел бы читать ее, а не по номинальной стоимости (и как ее прочитал бы кто-то, желающий писать приложения)...   -  person R.. GitHub STOP HELPING ICE    schedule 26.09.2011
comment
Я думаю, что это можно решить, поставив ожидающие потоки в очередь на локальном барьере процесса (или эквивалентном) перед возвратом, а последний оставит пробуждение остальных. это гарантировало бы, что ваш процесс больше не будет обращаться к объекту барьера, когда ожидание вернется, что сделает его безопасным для отмены сопоставления.   -  person Hasturkun    schedule 06.10.2011
comment
Проблема заключается в том, где хранить данные, необходимые им для согласования локального барьера процесса для ожидания. Внутри барьер не работает, потому что его может использовать сколько угодно много процессов. Глобальные данные в процессе не работают без использования >O(1) поисковых структур.   -  person R.. GitHub STOP HELPING ICE    schedule 06.10.2011


Ответы (3)


Это невозможно с Linux futex API, и я думаю, что это тоже можно доказать.

По сути, у нас есть сценарий, в котором N процессов должны быть надежно разбужены одним последним процессом, и, кроме того, ни один процесс не может касаться какой-либо общей памяти после окончательного пробуждения (поскольку она может быть уничтожена или повторно использована асинхронно). Хотя мы можем достаточно легко пробудить все процессы, фундаментальное состояние гонки находится между пробуждением и ожиданием; если мы вызовем пробуждение перед ожиданием, отставший никогда не проснется.

Обычное решение для чего-то подобного состоит в том, чтобы отставший проверял переменную состояния атомарно с ожиданием; это позволяет вообще не спать, если пробуждение уже произошло. Однако здесь мы этого сделать не можем — как только пробуждение становится возможным, трогать общую память небезопасно!

Еще один подход состоит в том, чтобы фактически проверить, все ли процессы уже перешли в спящий режим. Однако это невозможно с API фьютекса Linux; единственным показателем количества ожидающих является возвращаемое значение FUTEX_WAKE; если он возвращает меньшее количество официантов, чем вы ожидали, вы знаете, что некоторые из них еще не спали. Однако, даже если мы обнаружим, что разбудили недостаточное количество официантов, уже слишком поздно что-либо предпринимать — один из проснувшихся процессов, возможно, уже разрушил барьер!

Так что, к сожалению, этот тип примитива, который можно немедленно уничтожить, нельзя создать с помощью API фьютекса Linux.

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

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

Однако одним из возможных способов является расширение модели ключевого события в NT API. При событиях с ключом потоки освобождаются от блокировки парами; если у вас есть «релиз» без «ожидания», вызов «релиз» блокируется для «ожидания».

Этого самого по себе недостаточно из-за проблем с обработчиками сигналов; однако, если мы позволим вызову 'release' указать количество потоков, которые должны быть разбужены атомарно, это сработает. У вас просто есть каждый поток в барьере, уменьшающий счетчик, а затем «ждите» события с ключом по этому адресу. Последний поток "освобождает" N - 1 поток. Ядро не позволяет обрабатывать какие-либо события пробуждения до тех пор, пока все потоки N-1 не перейдут в это состояние события с ключом; если какой-либо поток покидает вызов фьютекса из-за сигналов (включая освобождающий поток), это вообще предотвращает любые пробуждения, пока все потоки не вернутся.

person bdonlan    schedule 24.09.2011
comment
Вот идея: можно использовать операцию FUTEX_REQUEUE (с одинаковыми адресами источника и получателя и 0 пробуждениями) для запроса количества ожидающих у фьютекса (она возвращает сумму числа разбуженных и возвращенных в очередь). Его также можно (предположительно) использовать для повторной постановки ожидающего в очередь на фьютекс, который даже не виден в адресном пространстве ожидающего. Возможно, этих операций достаточно, чтобы реализовать что-то вроде ключевых событий? Единственная большая проблема, я думаю, это то, что происходит, когда официанта прерывают по сигналу. Даже если вы заблокировали все сигналы (не совсем законно...) все равно остается SIGSTOP... - person R.. GitHub STOP HELPING ICE; 27.09.2011
comment
Запрос количества ожидающих по своей сути является колоритным - значение может измениться в момент возврата системного вызова, поэтому это бесполезно, кроме как для оптимизации производительности. И даже если вы начнете перемещать официантов по одному в локальную переменную фьютекса стека на стороне пробуждающего, официанты могут быть разбужены сигналом и затем вернутся к своему первоначальному ожиданию. Нет, я думаю, что для этого действительно нужна новая операция на стороне ядра. - person bdonlan; 27.09.2011
comment
Вы уверены, что они возобновят ожидание в исходной очереди, если их прервет сигнал? Может быть, так; Я должен проверить. Я скорее ожидал, что системный вызов ожидания фьютекса потерпит неудачу с EAGAIN или вернет 0 (пробуждение), если будет прерван после повторной постановки в очередь, аналогично тому, как read или write вернет частичную передачу, если будет прервано после чтения/записи некоторых данных. Конечно, чтобы быть уверенным, я должен действительно проверить это... - person R.. GitHub STOP HELPING ICE; 27.09.2011
comment
Другая идея, которая у меня была, заключалась в том, что операция pthread_barrier_init для общего барьера процесса должна была создать новый процесс, который был бы постоянным менеджером барьера и обменивался данными через фьютекс. Окончательным пробуждением можно было бы управлять, запрашивая какой-либо объект файловой системы, находящийся под управлением процесса менеджера, — что-то, что можно было бы запрашивать без необходимости выделения ресурсов, например access. Конечно, поскольку кажется, что нет способа заблокировать ожидание такого события, было бы желательно оптимизировать ожидание с помощью futex ops (идея выше) и использовать fs только для финальной проверки. - person R.. GitHub STOP HELPING ICE; 27.09.2011
comment
@R.., да, он потерпит неудачу и вернется EAGAIN. В этот момент барьерный код должен вернуться к ожиданию в исходной очереди. В конце концов, где еще он будет ждать? Затем рассмотрим гонку, в которой пробуждающий выдает сигнал FUTEX_WAKE в приватной очереди и возвращается из барьерной функции, а спящий пробуждается по сигналу и возвращается в ожидание в исходной очереди, хотя доступ к нему уже небезопасен. эта память. - person bdonlan; 27.09.2011
comment
Вот почему я думал о повторном доступе к фьютексу только в том случае, если access с R_OK на объекте файловой системы, находящемся под контролем процесса менеджера, удалось. Менеджер изменит его между режимом 444 и режимом 000 в зависимости от состояния, чтобы контролировать, что access вернет. Насколько мне известно, ни fchmod, ни access не должны давать сбой или возвращать неверные результаты из-за проблем с ресурсами. - person R.. GitHub STOP HELPING ICE; 27.09.2011
comment
Да, но если вы разрешаете доступ к внешним объектам, есть гораздо более простые решения — например, сопоставить файл из этой общей области только с помощью счетчика, когда он достигнет нуля, разбудить всех, разорвать связь, и каждый процесс отключится сам по себе. Или даже просто используйте сокет домена unix - первый процесс слушает и действует как мастер. - person bdonlan; 27.09.2011
comment
давайте продолжим это обсуждение в чате - person bdonlan; 27.09.2011

После долгого обсуждения с bdonlan в чате SO я думаю, что нашел решение. По сути, мы разбиваем проблему на две проблемы самосинхронизированного освобождения памяти: операцию уничтожения и размаппинг.

Обработка разрушения проста: просто заставьте функцию pthread_barrier_destroy ждать, пока все официанты перестанут проверять барьер. Это можно сделать с помощью счетчика использования в барьере, атомарно увеличивающегося/уменьшающегося при входе/выходе в функцию ожидания, и заставляя функцию уничтожения вращаться, ожидая, когда счетчик достигнет нуля. (Здесь также можно использовать фьютекс, а не просто вращение, если вы вставите флаг ожидания в старший бит счетчика использования или что-то подобное.)

Обработка отмены сопоставления также проста, но нелокальна: убедитесь, что munmap или mmap с флагом MAP_FIXED не может произойти, пока ожидающие барьера находятся в процессе выхода, добавив блокировку в оболочки системных вызовов. Для этого требуется специальный вид блокировки чтения-записи. Последний ожидающий, достигший барьера, должен захватить блокировку чтения на munmap rw-lock, которая будет освобождена, когда последний ожидающий выйдет (когда уменьшение счетчика пользователей приводит к счетчику, равному 0). munmap и mmap можно сделать реентерабельными (как могут ожидать некоторые программы, даже если POSIX этого не требует), сделав блокировку записи рекурсивной. На самом деле, тип блокировки, в котором читатели и писатели полностью симметричны, и каждый тип блокировки исключает блокировку противоположного типа, но не того же типа, должен работать лучше всего.

person R.. GitHub STOP HELPING ICE    schedule 27.09.2011

Ну, я думаю, что я могу сделать это с неуклюжим подходом...

Пусть «барьер» будет собственным процессом, прослушивающим сокет. Реализуйте барьер_ожидание как:

open connection to barrier process
send message telling barrier process I am waiting
block in read() waiting for reply

Как только N потоков находятся в ожидании, барьерный процесс сообщает им всем о продолжении. Затем каждый официант закрывает свое соединение с барьерным процессом и продолжает работу.

Реализуйте барьер_дестрой как:

open connection to barrier process
send message telling barrier process to go away
close connection

Как только все соединения закрыты и процессу барьера было приказано прекратить работу, он завершается.

[Редактировать: конечно, это выделяет и уничтожает сокет как часть операций ожидания и освобождения. Но я думаю, что вы можете реализовать тот же протокол и без этого; увидеть ниже.]

Первый вопрос: действительно ли этот протокол работает? Я думаю, что это так, но, возможно, я не понимаю требований.

Второй вопрос: если это работает, можно ли это смоделировать без накладных расходов на дополнительный процесс?

Я считаю, что ответ "да". Вы можете сделать так, чтобы каждый поток «взял на себя роль» барьерного процесса в соответствующее время. Вам просто нужен главный мьютекс, удерживаемый тем потоком, который в настоящее время «берет на себя роль» барьерного процесса. Детали, детали... Итак, барьер_ожидание может выглядеть так:

lock(master_mutex);
++waiter_count;
if (waiter_count < N)
    cond_wait(master_condition_variable, master_mutex);
else
    cond_broadcast(master_condition_variable);
--waiter_count;
bool do_release = time_to_die && waiter_count == 0;
unlock(master_mutex);
if (do_release)
    release_resources();

Здесь master_mutex (мьютекс), master_condition_variable (переменная условия), waiter_count (целое число без знака), N (другое целое число без знака) и time_to_die (логическое значение) — все они являются общим состоянием, выделяемым и инициализируемым барьером_инициализации. waiter_count инициализируется нулем, time_to_die — значением false, а N — количеством потоков, которых ожидает барьер.

Тогда барьер_дестрой будет:

lock(master_mutex);
time_to_die = true;
bool do_release = waiter_count == 0;
unlock(master_mutex);
if (do_release)
    release_resources();

Не уверен во всех деталях, касающихся обработки сигнала и т. д. Но основная идея «последний выключает свет», я думаю, работоспособна.

person Nemo    schedule 04.08.2011
comment
Открытое соединение — это операция, которая может завершиться ошибкой из-за слишком большого количества открытых файловых файлов в процессе или глобально в системе. :-( У меня есть гораздо более легкий (не требующий дополнительного процесса) способ создания барьера, когда первый официант создает fifo, а последующие блокируют попытки открыть его, но это все еще зависит от способности успешно создать fifo. Я предположим, что можно войти в бесконечный цикл, ожидая его успеха... - person R.. GitHub STOP HELPING ICE; 04.08.2011
comment
Вторая половина моего ответа не выделяет никаких ресурсов, кроме как во время инициализации ... Это работает? - person Nemo; 04.08.2011
comment
Ваш подход защищает от случая, когда гонка происходит между возвращением от барьера и его уничтожением, но я не думаю, что это вообще помогает, когда вызывающий отменяет отображение барьера, не разрушая его. Это, конечно, типичное использование для разделяемых процессами барьеров, когда вы можете захотеть оставить их для использования другими процессами. - person R.. GitHub STOP HELPING ICE; 04.08.2011
comment
@R..: Почему звонящий отключил бы барьер, не уничтожив его сначала? - person Hasturkun; 04.08.2011
comment
Потому что есть много процессов, использующих барьер. Он может принадлежать серверному процессу, с которым вы закончили общение, но которому все еще нужен барьер для последующего взаимодействия с другими процессами. - person R.. GitHub STOP HELPING ICE; 04.08.2011
comment
@R: В моем решении map/unmap - это ортогональная проблема... Все, что вам нужно сделать, это выяснить, как управлять созданием/уничтожением любой общей памяти, и все готово. Не обязательно тривиально, но это не имеет ничего общего с барьерами как таковыми. - person Nemo; 04.08.2011
comment
@R: Например, процесс, создающий барьер, может использовать shm_open+ftruncate+mmap для получения общей памяти, а затем сразу же shm_unlink. Затем он просто передает дескриптор файла любому процессу, которому требуется доступ к барьеру. Упомянутый процесс выполняет mmap(), а затем close() для дескриптора. Мастер делает close() после того, как все процессы получили fd. Любой процесс выполняет munmap(), когда захочет. Ядро освободит сегмент разделяемой памяти, как только последний процесс удалит его. - person Nemo; 04.08.2011
comment
Если вы собираетесь это сделать, первым ожидающим должен быть тот, кто создаст разделяемую память и передаст ее последующим ожидающим, что очень похоже на подход Майкла с контекстом экземпляра с автоматической длительностью хранения, но с выделением разделяемой памяти. Вы не можете выделить ресурс, как вы описали в функции инициализации барьера, потому что (1) у него не будет имени, когда последующие потоки попытаются получить к нему доступ, и (2) вы не можете гарантировать, что разрешения будут соответствовать разрешениям для разделяемая память семафора. - person R.. GitHub STOP HELPING ICE; 05.08.2011
comment
@R: Но это фундаментальные проблемы... кто-то должен инициализировать барьер. Это происходит только один раз. И только после этого имеет смысл использовать барьер. Итак, уже есть мастер-процесс, который должен инициализировать барьер и передать его другим процессам или каким-то образом уведомить их о готовности к использованию... - person Nemo; 05.08.2011