Потоки против процессов в Linux
Недавно я слышал, как некоторые люди говорят, что в Linux почти всегда лучше использовать процессы вместо потоков, так как Linux очень эффективен в обработке процессов и потому, что с потоками связано очень много проблем (таких как блокировка). Тем не менее, я подозреваю, потому что кажется, что потоки могут дать довольно большой прирост производительности в некоторых ситуациях.
Поэтому мой вопрос заключается в том, что когда я сталкиваюсь с ситуацией, когда потоки и процессы могут справиться с этим достаточно хорошо, мне следует использовать процессы или потоки? Например, если я писал веб-сервер, я должен использовать процессы или потоки (или их комбинацию)?
16 ответов
В Linux используется модель потоков 1-1, в которой (для ядра) нет различий между процессами и потоками - все это просто выполняемая задача. *
В Linux системный вызов clone
клонирует задачу с настраиваемым уровнем общего доступа, среди которых:
CLONE_FILES
: использовать одну и ту же таблицу дескрипторов файлов (вместо создания копии)CLONE_PARENT
: не устанавливайте родительско-дочерние отношения между новой задачей и старой (в противном случае дочерняяgetppid()
= родительскийgetpid()
)CLONE_VM
: использовать то же пространство памяти (вместо создания копии COW)
fork()
звонки clone(
наименьшее разделение)
а также pthread_create()
звонки clone(
большинство обмена)
, **
fork
Стоимость стоит чуть больше, чем pthread_create
Это связано с копированием таблиц и созданием отображений COW для памяти, но разработчики ядра Linux постарались (и преуспели) в минимизации этих затрат.
Переключение между задачами, если они совместно используют одно и то же пространство памяти и различные таблицы, будет стоить чуть-чуть дешевле, чем если бы они не были общими, потому что данные могут быть уже загружены в кэш. Однако переключение задач по-прежнему происходит очень быстро, даже если ничего не передается - это то, что разработчики ядра Linux пытаются обеспечить (и добиваются успеха в этом).
На самом деле, если вы работаете в многопроцессорной системе, отказ от совместного использования может быть полезен для производительности: если каждая задача выполняется на другом процессоре, синхронизация общей памяти обходится дорого.
* Упрощенный CLONE_THREAD
вызывает передачу сигналов для совместного использования (что необходимо CLONE_SIGHAND
, которая разделяет таблицу обработчиков сигналов).
** Упрощено. Там существуют оба SYS_fork
а также SYS_clone
системные вызовы, но в ядре sys_fork
а также sys_clone
оба очень тонкие обертки вокруг одного и того же do_fork
функция, которая сама по себе является тонкой оберткой вокруг copy_process
, Да, условия process
, thread
, а также task
используются довольно взаимозаменяемо в ядре Linux...
Linux (да и вообще Unix) дает вам третий вариант.
Вариант 1 - процессы
Создайте автономный исполняемый файл, который обрабатывает некоторую часть (или все части) вашего приложения, и вызывайте его отдельно для каждого процесса, например, программа запускает свои копии для делегирования задач.
Вариант 2 - темы
Создайте автономный исполняемый файл, который запускается с одного потока, и создайте дополнительные потоки для выполнения некоторых задач.
Вариант 3 - вилка
Доступно только под Linux/Unix, это немного отличается. Разветвленный процесс на самом деле является своим собственным процессом со своим собственным адресным пространством - дочерний элемент не может (обычно) ничего сделать, чтобы повлиять на адресное пространство своего родителя или братьев и сестер (в отличие от потока), поэтому вы получаете дополнительную надежность.
Однако страницы памяти не копируются, они копируются при записи, поэтому обычно используется меньше памяти, чем вы можете себе представить.
Рассмотрим программу веб-сервера, которая состоит из двух этапов:
- Чтение данных конфигурации и времени выполнения
- Обслуживать запросы страниц
Если вы используете потоки, шаг 1 будет выполнен один раз, а шаг 2 - в нескольких. Если вы использовали "традиционные" процессы, шаги 1 и 2 должны были бы повторяться для каждого процесса, а память для хранения данных конфигурации и времени выполнения дублировалась. Если вы использовали fork(), то вы можете выполнить шаг 1 один раз, а затем fork(), оставив данные и конфигурацию времени выполнения в памяти без изменений, не скопировав их.
Так что на самом деле есть три варианта.
Это зависит от многих факторов. Процессы более тяжелые, чем потоки, и имеют более высокую стоимость запуска и завершения работы. Межпроцессное взаимодействие (IPC) также сложнее и медленнее, чем межпотоковое взаимодействие.
И наоборот, процессы более безопасны и безопасны, чем потоки, поскольку каждый процесс выполняется в своем собственном виртуальном адресном пространстве. Если происходит сбой одного процесса или переполнение буфера, это никак не влияет на другие процессы, в то время как при сбое потока он удаляет все остальные потоки в процессе, а если поток имеет переполнение буфера, он открывается. дыра в безопасности во всех потоках.
Таким образом, если модули вашего приложения могут работать в основном независимо при небольшом обмене данными, вам, вероятно, следует использовать процессы, если вы можете позволить себе затраты на запуск и завершение работы. Падение производительности IPC будет минимальным, и вы будете немного безопаснее от ошибок и брешей в безопасности. Если вам нужен каждый бит производительности, который вы можете получить или иметь много общих данных (таких как сложные структуры данных), используйте потоки.
Другие обсуждали соображения.
Возможно, важным отличием является то, что в Windows процессы тяжелые и дорогие по сравнению с потоками, а в Linux разница намного меньше, поэтому уравнение балансирует в другой точке.
Когда-то был Unix, и в этом старом добром Unix было много накладных расходов на процессы, так что некоторые умные люди делали то, что создавали потоки, которые совместно использовали бы то же адресное пространство с родительским процессом, и им нужен был только сокращенный контекст переключатель, который сделает переключение контекста более эффективным.
В современном Linux (2.6.x) не так много различий в производительности между переключением контекста процесса по сравнению с потоком (только поток MMU является дополнительным для потока). Существует проблема с общим адресным пространством, что означает, что неисправный указатель в потоке может повредить память родительского процесса или другого потока в том же адресном пространстве.
Процесс защищен MMU, поэтому неисправный указатель просто вызовет сигнал 11 и не повредит.
В общем, я бы использовал процессы (не слишком много издержек при переключении контекста в Linux, но защита памяти из-за MMU), но я бы подумал, если мне понадобится класс планировщика реального времени, который представляет собой другую чашку чая.
Как вы думаете, почему потоки имеют такой большой прирост производительности в Linux? У вас есть данные для этого, или это просто миф?
Я думаю, что все проделали отличную работу, отвечая на ваш вопрос. Я просто добавляю больше информации о потоке против процесса в Linux, чтобы прояснить и обобщить некоторые предыдущие ответы в контексте ядра. Итак, мой ответ касается специфичного для ядра кода в Linux. Согласно документации ядра Linux, нет четкого различия между потоком и процессом, за исключением того, что поток использует общее виртуальное адресное пространство в отличие от процесса. Также обратите внимание, что ядро Linux использует термин "задача" для обозначения процесса и потока в целом.
"Нет внутренних структур, реализующих процессы или потоки, вместо этого есть структура task_struct, которая описывает абстрактный блок планирования, называемый задачей"
Кроме того, согласно Линусу Торвальдсу, вы НЕ должны вообще думать о процессе и потоке, поскольку он слишком ограничен, и единственное отличие - это COE или контекст выполнения с точки зрения "отделения адресного пространства от родительского" или общего адресного пространства. На самом деле он использует пример веб-сервера, чтобы высказать свою точку зрения (который настоятельно рекомендуется прочитать).
Полный кредит документации ядра Linux
Если вы хотите создать максимально чистый процесс, вы должны использовать clone() и установить все флаги клонирования. (Или избавьтесь от необходимости печатать и позвоните
fork()
)
Если вы хотите создать чистый поток, насколько это возможно, вы должны использовать clone() и сбросить все флаги клонирования (или сэкономить время на вводе текста и вызвать
pthread_create()
)
Есть 28 флагов, которые определяют уровень обмена. Это означает, что вы можете создать более 268 миллионов разновидностей задач, в зависимости от того, чем вы хотите поделиться.
Это то, что мы имеем в виду, когда говорим, что Linux не различает процесс и поток, а скорее намекает на любой поток управления в программе как на задачу. Причина, по которой нельзя различать эти два вкуса, не однозначно определяет более 268 миллионов вкусов!
Следовательно, принятие "идеального решения" о том, использовать ли процесс или поток, на самом деле означает решить, какой из 28 ресурсов клонировать.
Насколько тесно связаны ваши задачи?
Если они могут жить независимо друг от друга, то использовать процессы. Если они полагаются друг на друга, то используйте темы. Таким образом, вы можете убить и перезапустить плохой процесс, не мешая работе других задач.
В моей недавней работе с LINUX важно знать о библиотеках. Если вы используете потоки, убедитесь, что все библиотеки, которые вы можете использовать между потоками, являются поточно-ориентированными. Это обожгло меня пару раз. Примечательно, что libxml2 не поддерживает потоки из коробки. Он может быть скомпилирован с потокобезопасностью, но это не то, что вы получаете при установке aptitude.
Чтобы еще больше усложнить ситуацию, существует такая вещь, как локальное хранилище потоков и общая память Unix.
Локальное хранилище потоков позволяет каждому потоку иметь отдельный экземпляр глобальных объектов. Единственный раз, когда я использовал его, был при создании среды эмуляции в linux/windows для кода приложения, работающего в RTOS. В ОСРВ каждая задача была процессом с собственным адресным пространством, в среде эмуляции каждая задача была потоком (с общим адресным пространством). Используя TLS для таких вещей, как синглеты, мы смогли создать отдельный экземпляр для каждого потока, как в "реальной" среде RTOS.
Совместно используемая память может (очевидно) дать вам преимущества в производительности, если несколько процессов имеют доступ к одной и той же памяти, но при этом стоит риск / риск правильной синхронизации процессов. Один из способов сделать это состоит в том, чтобы один процесс создал структуру данных в разделяемой памяти, а затем отправил дескриптор этой структуры посредством традиционного межпроцессного взаимодействия (например, именованного канала).
Решение между потоком / процессом немного зависит от того, для чего вы будете его использовать. Одно из преимуществ процесса заключается в том, что он имеет PID и может быть уничтожен без удаления родителя.
Для реального примера веб-сервера apache 1.3 использовался только для поддержки нескольких процессов, но в 2.0 он добавил абстракцию, так что вы можете переключаться между ними. Похоже, комментарии согласны с тем, что процессы более устойчивы, но потоки могут дать немного лучшую производительность (за исключением окон, в которых производительность процессов отстой и вы хотите использовать только потоки).
Я должен согласиться с тем, что вы слышали. Когда мы измеряем наш кластер (xhpl
и так далее), мы всегда получаем значительно лучшую производительность с процессами над потоками. </anecdote>
Потоки - > Потоки разделяют пространство памяти, это абстракция ЦП, она легкая. Процессы - > Процессы имеют собственное пространство памяти, это абстракция компьютера. Чтобы распараллелить задачу, вам нужно абстрагировать процессор. Однако преимуществом использования процесса над потоком является безопасность, стабильность, в то время как поток использует меньше памяти, чем процесс, и обеспечивает меньшую задержку. Примером с точки зрения сети будет Chrome и Firefox. В случае Chrome каждая вкладка - это новый процесс, поэтому использование памяти в chrome выше, чем в Firefox, а обеспеченная безопасность и стабильность лучше, чем в Firefox. Безопасность, обеспечиваемая Chrome, лучше, поскольку каждая вкладка - это новый процесс, который не может быть отслежен в пространстве памяти данного процесса.
В большинстве случаев я бы предпочел процессы над потоками. Потоки могут быть полезны, когда у вас относительно небольшая задача (издержки процесса >> время, затрачиваемое каждым разделенным блоком задач) и существует необходимость в распределении памяти между ними. Подумайте, большой массив. Также (оффтоп), обратите внимание, что если загрузка вашего процессора составляет 100 процентов или близка к ней, многопоточность или обработка не принесут никакой пользы. (на самом деле это ухудшится)
Многопоточность - для мазохистов. :)
Если вас беспокоит среда, в которой вы постоянно создаете потоки / форки, например, как веб-сервер, обрабатывающий запросы, вы можете предварительно выполнить форк процессов, сотни при необходимости. Поскольку они копируют при записи и используют одну и ту же память до тех пор, пока не произойдет запись, это очень быстро. Все они могут блокироваться, прослушивая один и тот же сокет, и тот, кто первым принимает входящее TCP-соединение, запускается с ним. С помощью g++ вы также можете назначать функции и переменные, которые должны быть близко размещены в памяти (горячие сегменты), чтобы гарантировать, что когда вы действительно выполняете запись в память, и заставляете копировать всю страницу, по крайней мере, последующие действия записи будут происходить на той же странице. Вам действительно нужно использовать профилировщик для проверки такого рода вещей, но если вас беспокоит производительность, вы все равно должны это делать.
Время разработки многопоточных приложений в 3-10 раз больше из-за тонкого взаимодействия с общими объектами, потоковых «ошибок», о которых вы не задумывались, и очень трудно отлаживать, потому что вы не можете воспроизвести проблемы взаимодействия потоков по своему желанию. Возможно, вам придется выполнять всевозможные проверки производительности, такие как наличие инвариантов во всех ваших классах, которые проверяются до и после каждой функции, и вы останавливаете процесс и загружаете отладчик, если что-то не так. Чаще всего это неприятные сбои, которые происходят во время производства, и вам приходится просматривать дамп ядра, пытаясь выяснить, какие потоки что сделали. Откровенно говоря, не стоит головной боли, когда процессы разветвления столь же быстры и неявно потокобезопасны, если вы явно не делитесь чем-то.По крайней мере, при явном совместном использовании вы точно знаете, где искать, если возникает проблема стиля потоковой передачи.
Если производительность так важна, добавьте еще один компьютер и распределите нагрузку. Из-за затрат разработчика на отладку многопоточного приложения, даже если оно написано опытным многопоточным разработчиком, вы, вероятно, могли бы купить 4 40-ядерные материнские платы Intel с 64 ГБ памяти каждая.
При этом существуют асимметричные случаи, когда параллельная обработка не подходит, например, вы хотите, чтобы поток переднего плана принимал ввод пользователя и немедленно отображал нажатия кнопок, не дожидаясь, пока какой-то неуклюжий графический интерфейс на бэкэнд не успеет. Сексуальное использование потоков там, где многопроцессорность геометрически неуместна. Многие подобные вещи просто переменные или указатели. Это не «дескрипторы», которые можно использовать в вилке. Вы должны использовать потоки. Даже если бы вы сделали форк, вы бы использовали один и тот же ресурс и столкнулись бы с проблемами стиля потоковой передачи.
Если вам нужно делиться ресурсами, вы действительно должны использовать потоки.
Также учтите тот факт, что переключение контекста между потоками намного дешевле, чем переключение контекста между процессами.
Я не вижу причин для явного перехода к отдельным процессам, если у вас нет для этого веских причин (безопасность, проверенные тесты производительности и т. Д.)