#multithreading #asynchronous #network-programming #scheduler
#многопоточность #асинхронный #сетевое программирование #планировщик
Вопрос:
В большинстве случаев вы услышите, что модель thread-per-multiple-connections (неблокирующий ввод-вывод) намного лучше, чем модель thread-per-connection (блокирующий ввод-вывод). И рассуждения звучат как «Подход «Поток на соединение» создает слишком много потоков, и с обслуживанием такого количества потоков связано много накладных расходов». Но эти накладные расходы не объясняются.
- Распространенное заблуждение заключается в том, что накладные расходы на планирование пропорциональны количеству всех потоков. Но это не так, накладные расходы на планирование пропорциональны количеству выполняемых потоков. Таким образом, в типичном приложении, связанном с вводом-выводом, большинство потоков фактически будут заблокированы при вводе-выводе, и только некоторые из них будут доступны для выполнения — что не отличается от модели «поток на несколько соединений».
- Что касается накладных расходов на переключение контекста, я ожидаю, что разницы быть не должно, потому что при поступлении данных ядро должно разбудить поток выбора потока или поток подключения.
- Проблема может заключаться в системных вызовах ввода-вывода — ядро может обрабатывать вызовы kqueue / epoll лучше, чем блокирование вызовов ввода-вывода. Однако это звучит неправдоподобно, потому что не должно быть проблемой реализовать алгоритм O (1) для выбора заблокированного потока при поступлении данных.
- Если у вас много недолговечных соединений, у вас будет много недолговечных потоков. И создание нового потока — дорогостоящая операция (не так ли?). Чтобы решить эту проблему, вы можете создать пул потоков и по-прежнему использовать блокирующий ввод-вывод.
- Могут быть ограничения ОС на количество потоков, которые могут быть созданы, однако они могут быть изменены с помощью параметров конфигурации.
- В многоядерной системе предположим, что разные сеансы обращаются к одним и тем же общим данным. Если мы говорим о модели connection-per-thread, это может привести к большому трафику согласованности кэша и может замедлить работу системы. Однако почему бы не распределить все эти потоки на одном ядре, если только один из них может быть запущен в данный момент времени? Если более одного из них можно запускать, это означает, что они должны быть запланированы на разных ядрах. Однако для достижения одинаковой производительности в модели thread-per-multiple connections нам потребуется несколько селекторов, и они будут запланированы на разных ядрах и будут получать доступ к одним и тем же общим данным. Так что я не вижу различий с точки зрения кэша.
- В среде GC (возьмем, к примеру, Java) сборщик мусора должен понимать, какие объекты достижимы путем обхода графа объектов, начиная с корней GC. Корни GC включают стеки потоков. Таким образом, GC предстоит выполнить больше работы на первом уровне этого графика. Однако общее количество активных узлов в этом графике должно быть одинаковым для обоих подходов. Так что никаких накладных расходов с точки зрения GC.
- Единственный аргумент, с которым я согласен, заключается в том, что каждый поток потребляет память для своего стека. Но даже в этом случае мы можем ограничить размер стеков для этих потоков, если они не используют рекурсивные вызовы.
Что вы думаете?
Комментарии:
1. Два новых пункта, которые вы только что добавили, также верны. Обратите внимание, что неблокирующий ввод-вывод не подразумевает один поток. Обычно это один поток на ядро для серверов, которые предназначены для масштабирования. Однопоточный
select
илиpoll
основанный на нем дизайн в основном устарел. Современные проекты, такие как epoll, kqueue или IOCP, используют несколько потоков для слива очереди событий ввода-вывода (по существу).2. @usr, спасибо за ваш ответ, я добавляю новые моменты, когда они приходят мне на ум. Я надеюсь, что этот вопрос поможет многим людям понять, что это единственная накладная расходы стека потоков, которая должна влиять на их решение.
3. Соломенный человечек. Я не знаю, чтобы кто-нибудь утверждал, что поток на соединение медленнее в соответствии с вашим заголовком, и если они ошибаются. Обычные утверждения, которые, я согласен, сомнительны, заключаются в том, что NBIO, async и т. Д. Более масштабируемы, и это то, о чем на самом деле говорится в тексте вашего сообщения.
4. @EJP, я согласен, что масштабируемость лучше описывает эти утверждения. Я изменил название.
Ответ №1:
Есть два накладных расходов:
- Стековая память. Неблокирующий ввод-вывод (в какой бы форме вы его ни использовали) экономит память стека. Теперь IO — это всего лишь небольшая структура данных.
- Сокращение переключения контекста и переходов ядра при высокой нагрузке. Затем один коммутатор можно использовать для обработки нескольких завершенных iOS.
Большинство серверов не находятся под высокой нагрузкой, потому что это оставило бы небольшой запас прочности от скачков нагрузки. Таким образом, пункт (2) актуален в основном для искусственных нагрузок, таких как тесты (предназначенные для доказательства точки …).
Экономия стека — это 99% причина, по которой это делается.
Хотите ли вы сэкономить время разработки и сложность кода для экономии памяти, зависит от того, сколько у вас подключений. При 10 соединениях это не вызывает беспокойства. При 10000 подключениях модель, основанная на потоках, становится неосуществимой.
Пункты, которые вы указываете в вопросе, верны.
Может быть, вас смущает тот факт, что «здравый смысл» заключается в том, чтобы всегда использовать неблокирующий ввод-вывод сокета? Действительно, эта (ложная) пропаганда распространяется повсюду в Интернете. Пропаганда работает, многократно делая одно и то же простое утверждение, и оно работает.