Безопасно ли разветвляться изнутри? - PullRequest
39 голосов
/ 21 мая 2011

Позвольте мне объяснить: я уже разрабатывал приложение для Linux, которое разветвляет и исполняет внешний двоичный файл и ожидает его завершения. Результаты передаются с помощью файлов shm, уникальных для процесса fork +. Весь код инкапсулирован в классе.

Теперь я подумываю о многопоточности процесса, чтобы ускорить процесс. Наличие множества различных экземпляров функций класса одновременно запускает и исполняет двоичный файл (с разными параметрами) и передает результаты в свои собственные уникальные файлы shm.

Безопасен ли этот поток? Если я разветвляюсь в потоке, кроме безопасности, есть ли что-то, за чем я должен следить? Любой совет или помощь высоко ценится!

Ответы [ 7 ]

54 голосов
/ 21 мая 2011

Проблема в том, что fork () копирует только вызывающий поток, и любые мьютексы, содержащиеся в дочерних потоках, будут навсегда заблокированы в разветвленном дочернем элементе.Решением pthread были pthread_atfork() обработчики.Идея заключалась в том, что вы можете зарегистрировать 3 обработчика: один prefork, один родительский обработчик и один дочерний обработчик.Когда fork() происходит, prefork вызывается перед fork и, как ожидается, получит все мьютексы приложения.И родитель, и ребенок должны освободить все мьютексы в родительском и дочернем процессах соответственно.

Это еще не конец истории!Библиотеки вызывают pthread_atfork, чтобы зарегистрировать обработчики для специфичных для библиотеки мьютексов, например, Libc делает это.Это хорошо: приложение не может знать о мьютексах, хранящихся в сторонних библиотеках, поэтому каждая библиотека должна вызывать pthread_atfork, чтобы убедиться, что ее собственные мьютексы очищены в случае fork().

Проблема в том, что порядок вызова обработчиков pthread_atfork для несвязанных библиотек не определен (это зависит от порядка загрузки библиотек программой).Таким образом, это означает, что технически тупик может возникнуть внутри обработчика префорков из-за состояния гонки.

Например, рассмотрим следующую последовательность:

  1. Поток T1 вызывает fork()
  2. Обработчики преформ libc вызываются в T1 (например, T1 теперь содержит все блокировки libc)
  3. Затем в потоке T2 сторонняя библиотека A получает собственный мьютекс AM, а затем выполняет вызов libc, для которого требуется мьютекс.Это блокирует, потому что мьютексы libc хранятся в T1.
  4. Поток T1 запускает обработчик prefork для библиотеки A, которая блокирует ожидание получения AM, который удерживается T2.

тупик и не связанный с вашими мьютексами или кодом.

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

10 голосов
/ 21 мая 2011

Безопасно разветвляться в многопоточной программе, если вы очень осторожны в отношении кода между fork и exec. В этом диапазоне вы можете делать только входящие (асинхронно безопасные) системные вызовы. Теоретически, вам не разрешено malloc или free там, хотя на практике распределитель Linux по умолчанию безопасен, и библиотеки Linux стали полагаться на него. В результате вы должны использовать распределитель по умолчанию.

6 голосов
/ 21 мая 2011

В то время как вы можете использовать поддержку NPTL pthreads(7) в Linux для своей программы, потоки неуклюже подходят для систем Unix, как вы обнаружили с вашим вопросом fork(2).

Поскольку fork(2) является очень дешевой операцией в современных системах, вам может быть лучше просто fork(2) ваш процесс, когда у вас есть больше возможностей для обработки.Это зависит от того, сколько данных вы намереваетесь перемещать взад и вперед, философия fork ed, не требующая общего доступа, хороша для уменьшения ошибок совместно используемых данных, но означает, что вам либо необходимо создать каналы для перемещения данных между процессами или использовать разделяемую память (shmget(2) или shm_open(3)).

Но если вы решите использовать многопоточность, вы сможете fork(2) новый процесс со следующими подсказкамииз справочной страницы fork(2):

   *  The child process is created with a single thread — the
      one that called fork().  The entire virtual address space
      of the parent is replicated in the child, including the
      states of mutexes, condition variables, and other pthreads
      objects; the use of pthread_atfork(3) may be helpful for
      dealing with problems that this can cause.
3 голосов
/ 21 мая 2011

Когда-то на заре времени мы называли потоки «облегченными процессами», потому что, хотя они во многом похожи на процессы, они не идентичны.Самым большим отличием является то, что потоки по определению живут в одном и том же адресном пространстве одного процесса.Это имеет свои преимущества: быстрое переключение с одного потока на другое, они по своей природе совместно используют память, поэтому связь между потоками происходит быстро, а создание и удаление потоков происходит быстро.

Различие здесь в «тяжелых процессах», которыеявляются полными адресными пространствами.Новый тяжеловесный процесс создается fork (2) .Когда виртуальная память вошла в мир UNIX, она была дополнена vfork (2) и некоторыми другими.

A fork (2) копирует все адресное пространствоэтот процесс, включая все регистры, помещает этот процесс под контроль планировщика операционной системы;в следующий раз, когда планировщик приходит в себя, счетчик команд запускается при следующей инструкции - разветвленный дочерний процесс является клоном родительского.(Если вы хотите запустить другую программу, скажем, потому что вы пишете оболочку, вы следуете за ответвлением с вызовом exec (2) , который загружает это новое адресное пространство новой программой, заменяя однуэто было клонировано.)

По сути, ваш ответ скрыт в этом объяснении: когда у вас есть процесс со многими LWPs потоками, и вы разветвляете процесс, у вас будет два независимых процесса со многимипотоки, работающие одновременно.

Этот прием даже полезен: во многих программах у вас есть родительский процесс, который может иметь много потоков, некоторые из которых разветвляют новые дочерние процессы.(Например, HTTP-сервер может сделать это: каждое соединение с портом 80 обрабатывается потоком, а затем дочерний процесс для чего-то вроде программы CGI может быть разветвлен; exec (2) будет тогдавызывается для запуска программы CGI вместо закрытия родительского процесса.)

1 голос
/ 21 мая 2011

Если вы быстро либо вызовете exec, либо _exit в дочернем дочернем процессе, то на практике все нормально.

Возможно, вы захотите использовать вместо этого posix_spawn (), что, вероятно, сделает правильно.

0 голосов
/ 21 мая 2011

fork IN, даже с резьбой, является безопасным. После ветвления потоки становятся независимыми для каждого процесса. (То есть многопоточность ортогональна к разветвлению). Однако, если потоки в разных процессах используют одну и ту же разделяемую память для связи, вам необходимо разработать механизм синхронизации.

0 голосов
/ 21 мая 2011

Если вы используете системный вызов unix 'fork ()', то с технической точки зрения вы не используете потоки - вы используете процессы - они будут иметь свое собственное пространство памяти и, следовательно, не смогут влиять друг на друга.

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

...