Параллелизм против цикла событий против цикла событий + параллелизм

Прежде всего, давайте объясним терминологию.
Параллелизм - означает, что у вас есть несколько очередей задач на нескольких ядрах / потоках процессора. Но это полностью отличается от параллельного выполнения, параллельное выполнение не будет содержать очереди нескольких задач для параллельного случая, нам потребуется 1 ядро ​​ЦП / поток на задачу для полного параллельного выполнения, что в большинстве случаев мы не можем определить. Вот почему для современной разработки программного обеспечения параллельное программирование иногда означает «параллелизм», я знаю это странно, но очевидно, что это то, что у нас есть на данный момент (это зависит от модели процессора / потока ОС).
Цикл событий - Означает однопоточный бесконечный цикл, который выполняет одну задачу за раз, и он не только создает очередь одной задачи, но также определяет приоритеты задач, потому что с циклом событий у вас есть только один ресурс для выполнения (1 поток), поэтому для выполнения для некоторых задач сразу нужно расставить приоритеты. В некоторых словах этот подход к программированию называется Thread Safe Programming, потому что одновременно может выполняться только одна задача / функция / операция, и если вы что-то меняете, это уже будет изменено во время следующего выполнения задачи.

Параллельное программирование

В современных компьютерах / серверах у нас минимум 2 ядра ЦП и мин. 4 потока ЦП. Но на серверах сейчас средн. сервер имеет не менее 16 потоков ЦП. Поэтому, если вы пишете программное обеспечение, которое требует некоторой производительности, вам обязательно следует подумать о том, чтобы сделать его таким образом, чтобы оно использовало все ядра ЦП, доступные на сервере.

Это изображение отображает базовую модель параллелизма, но, конечно, не так просто, чтобы она отображалась :)

Параллельное программирование становится действительно сложным с некоторыми общими ресурсами, например, давайте взглянем на этот Go простой параллельный код.

// Wrong concurrency with Go language
package main
import (
   "fmt"
   "time"
)
var SharedMap = make(map[string]string)
func changeMap(value string) {
    SharedMap["test"] = value
}
func main() {
    go changeMap("value1")
    go changeMap("value2")
    time.Sleep(time.Millisecond * 500)
    fmt.Println(SharedMap["test"])
}
// This will print "value1" or "value2" we don't know exactly!

В этом случае Go будет запускать 2 одновременных задания, вероятно, для разных ядер ЦП, и мы не можем предсказать, какое из них будет выполнено первым, поэтому мы не знаем, что будет отображаться в конце.
Почему? - Все просто! Мы планируем две разные задачи для разных ядер ЦП, но они используют одну общую переменную / память, поэтому они обе изменяют эту память, а в некоторых случаях это может произойти в случае сбоя / исключения программы.

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

Большинству разработчиков не нравится параллельное программирование, потому что параллелизм не всегда означает производительность. Это зависит от конкретных случаев.

Однопоточный цикл событий

Такой подход к разработке программного обеспечения намного проще, чем программирование с параллелизмом. Потому что принцип очень простой. У вас одновременно выполняется только одна задача. И в этом случае у вас нет проблем с общими переменными / памятью, потому что программа более предсказуема с одной задачей за раз.

Общий процесс следующий
1. Эмиттер событий добавляет задачу в очередь событий для выполнения в следующем цикле цикла
2. Цикл событий получает задачу из очереди событий и обрабатывает ее на основе обработчиков

Давайте напишем тот же пример с node.js

let SharedMap = {};
const changeMap = (value) => {
    return () => {
        SharedMap["test"] = value
    }
}
// 0 Timeout means we are making new Task in Queue for next cycle
setTimeout(changeMap("value1"), 0);
setTimeout(changeMap("value2"), 0);
setTimeout(()=>{
   console.log(SharedMap["test"])
}, 500);
// in this case Node.js will print "value2" because it is single 
// threaded and it have "only one task queue"

Как вы можете себе представить, в этом случае код более предсказуем, чем в параллельном примере Go, и это потому, что Node.js работает в однопоточном режиме с использованием цикла событий JavaScript.

В некоторых случаях цикл событий дает больше производительности, чем при параллелизме, из-за Non Blocking Behavior. Очень хороший пример - сетевые приложения, потому что они используют один ресурс сетевого подключения и обрабатывают данные только тогда, когда они доступны с помощью потоковых циклов событий.

Параллелизм + цикл событий - пул потоков с безопасностью потоков

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

Давайте посмотрим на модель Thread Pool + Event Loop из Nginx Web Server Structure

Основную обработку сети и конфигурации выполняет Worker Event Loop in a single thread for safety, но когда Nginx необходимо прочитать какой-либо файл или обработать заголовки / тело HTTP-запроса, которые блокируют операции, он отправляет эту задачу в свой пул потоков для параллельной обработки. И когда задача выполнена, результат отправляется обратно в цикл обработки событий для выполнения поточно-безопасной обработки.

Таким образом, используя эту структуру, вы получаете как безопасность потоков, так и параллелизм, что позволяет использовать все ядра ЦП для повышения производительности и сохранять принцип неблокирования с однопоточным циклом обработки событий.

Заключение

Многие программы пишутся с чистым параллелизмом или с чистым однопоточным циклом обработки событий, но объединение обоих внутри одного приложения упрощает создание высокопроизводительных приложений и использование всех доступных ресурсов ЦП.