Правильное отказобезопасности процесс-общий барьер быть реализовано в Linux?

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

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

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

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

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

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

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

3 ответов


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

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

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

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

таким образом, к сожалению, этот вид немедленно разрушаемого примитива не может быть построен с Linux API для фьютекс.

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

сложно добавить надежное расширение к модели futex, которое бы это исправило. Основная проблема заключается в том, что нам нужно знать, когда n потоков успешно вошли в ожидание, и атомарно разбудить их всех. Однако любой из этих потоков может оставить ожидание запуска обработчика сигнала в любое время - действительно, поток waker также может оставить ожидание обработчиков сигналов.

один из возможных способов, который может работать, однако, является расширением кузова событие модель в API NT. С ключевыми событиями потоки освобождаются из блокировки попарно; если у вас есть "релиз" без "wait", "release" блокирует вызов "wait".

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


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

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

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


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

имейте "барьер" быть своим собственным процессом слушая на гнезде. Осуществлять barrier_wait как:

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

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

реализовать barrier_destroy как:

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

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

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

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

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

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

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 (a Boolean) - все общее состояние, выделенное и инициализированное barrier_init. waiter_count это initialiazed к нулю, time_to_die false, а N к числу потоков барьер ждет.

тогда barrier_destroy будет:

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

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