Безопасно ли разветвляться изнутри?

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

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

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

7 ответов

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

поскольку fork(2) это очень дешевая операция на современных системах, вы могли бы лучше просто fork(2) ваш процесс, когда у вас есть больше обработки, чтобы выполнить. Это зависит от того, сколько данных вы намереваетесь перемещать взад и вперед, философию "без долей fork Редактирование процессов хорошо для уменьшения ошибок общих данных, но означает, что вам нужно либо создавать каналы для перемещения данных между процессами, либо использовать общую память (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.

Мой опыт fork()внутри потоков действительно плохо. Программное обеспечение обычно довольно быстро выходит из строя.

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

  1. Сначала вилка

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

    Как только вы разветвляете достаточно детей, вы можете использовать потоки и общаться с этими разветвленными процессами через ваши каналы, семафоры и т. Д. С момента создания первого потока вы больше не можете вызывать fork. Имейте в виду, что если вы используете сторонние библиотеки, которые могут создавать потоки, их нужно использовать / инициализировать послеfork() звонки случились.

    Обратите внимание, что затем вы можете начать использовать потоки в основном и fork()'ed процессы.

  2. Знай свое состояние

    В некоторых случаях вы можете остановить все свои потоки, чтобы запустить процесс, а затем перезапустить потоки. Это несколько похоже на пункт (1) в том смысле, что вы не хотите, чтобы потоки выполнялись в то время, когда вы вызываетеfork(), хотя для этого вам нужен способ узнать обо всех потоках, запущенных в настоящее время в вашем программном обеспечении (что не всегда возможно со сторонними библиотеками).

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

  3. Выберите один или другой

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

В своем программировании я использовал все три решения. Я использовал Point (2), потому что многопоточная версияlog4cplus и мне нужно было использовать fork() для некоторых частей моего программного обеспечения.

Как уже упоминалось другими, если вы используете fork() затем позвонить execve()тогда идея состоит в том, чтобы использовать как можно меньше между двумя вызовами. Вероятно, это сработает в 99,999% случаев (многие люди используютsystem() или popen() с довольно хорошими успехами, и они делают аналогичные вещи).

С другой стороны, если, как и я, вы хотите сделать fork() и никогда не звони execve(), то вряд ли он будет работать правильно, пока работает какой-либо поток.

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

Вы могли бы хотеть использовать posix_spawn() вместо этого, который вероятно сделает Правильную вещь.

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

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

Другие вопросы по тегам