Если вам нужно задать этот вопрос, то вы, вероятно, не знакомы с тем, что делает большинство веб-приложений / сервисов. Вы, вероятно, думаете, что все программное обеспечение делает это:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
Тем не менее, это не то, как работают веб-приложения или даже любые приложения с базой данных в качестве фоновой. Веб-приложения делают это:
user do an action
│
v
application start processing action
└──> make database request
└──> do nothing until request completes
request complete
└──> send result to user
В этом сценарии программное обеспечение тратит большую часть своего рабочего времени, используя 0% времени ЦП, ожидая возврата базы данных.
Многопоточное сетевое приложение:
Многопоточные сетевые приложения обрабатывают вышеуказанную рабочую нагрузку следующим образом:
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
request ──> spawn thread
└──> wait for database request
└──> answer request
Поэтому поток тратит большую часть своего времени, используя 0% ЦП, ожидая, пока база данных вернет данные. При этом им пришлось выделять память, требуемую для потока, который включает в себя совершенно отдельный программный стек для каждого потока и т. Д. Кроме того, им пришлось бы запускать поток, который хотя и не так дорог, поскольку запуск полного процесса все еще не совсем точен. дешево.
Однопоточный цикл событий
Поскольку мы проводим большую часть нашего времени, используя 0% CPU, почему бы не запустить некоторый код, когда мы не используем CPU? Таким образом, каждый запрос все равно будет занимать столько же процессорного времени, что и многопоточные приложения, но нам не нужно запускать поток. Итак, мы делаем это:
request ──> make database request
request ──> make database request
request ──> make database request
database request complete ──> send response
database request complete ──> send response
database request complete ──> send response
На практике оба подхода возвращают данные с примерно одинаковой задержкой, поскольку именно время отклика базы данных доминирует в обработке.
Основным преимуществом здесь является то, что нам не нужно создавать новый поток, поэтому нам не нужно делать много-много malloc, которые замедляют нас.
Волшебная, невидимая нить
Казалось бы, загадочная вещь заключается в том, как обоим вышеописанным подходам удается выполнять рабочую нагрузку «параллельно»? Ответ в том, что база данных является многопоточной. Таким образом, наше однопоточное приложение фактически использует многопоточное поведение другого процесса: базы данных.
Где однопоточный подход терпит неудачу
Однопоточное приложение перестает работать, если вам нужно выполнить много вычислений ЦП перед возвратом данных. Я не имею в виду цикл обработки результата базы данных. Это по-прежнему в основном O (N). Я имею в виду такие вещи, как преобразование Фурье (например, кодирование mp3), трассировка лучей (3D-рендеринг) и т. Д.
Еще одна ловушка однопоточных приложений заключается в том, что они будут использовать только одно ядро процессора. Так что, если у вас есть четырехъядерный сервер (в наши дни это не редкость), вы не используете остальные 3 ядра.
Где многопоточный подход терпит неудачу
Многопоточное приложение перестает работать, если вам нужно выделить много ОЗУ для каждого потока. Во-первых, само использование ОЗУ означает, что вы не можете обрабатывать столько запросов, сколько и однопоточное приложение. Хуже того, malloc работает медленно. Распределение множества объектов (что характерно для современных веб-фреймворков) означает, что мы можем оказаться медленнее, чем однопоточные приложения. Именно здесь node.js обычно побеждает.
Один из вариантов использования, который в конечном итоге ухудшает многопоточность, - это когда вам нужно запустить другой язык сценариев в вашей ветке. Сначала вам нужно распределить всю среду выполнения для этого языка, а затем распределить переменные, используемые вашим сценарием.
Так что, если вы пишете сетевые приложения на C или Go или Java, то издержки на многопоточность обычно не так уж велики. Если вы пишете веб-сервер C для обслуживания PHP или Ruby, тогда очень просто написать более быстрый сервер на javascript или Ruby или Python.
Гибридный подход
Некоторые веб-серверы используют гибридный подход. Например, Nginx и Apache2 реализуют свой код сетевой обработки как пул потоков циклов событий. Каждый поток выполняет цикл обработки событий, одновременно обрабатывая запросы однопоточными, но запросы сбалансированы между несколькими потоками.
Некоторые однопоточные архитектуры также используют гибридный подход. Вместо запуска нескольких потоков из одного процесса вы можете запускать несколько приложений, например, 4 сервера node.js на четырехъядерном компьютере. Затем вы используете балансировщик нагрузки, чтобы распределить рабочую нагрузку между процессами.
По сути, два подхода являются технически идентичными зеркальными отображениями друг друга.