Jeśli musisz zadać to pytanie, prawdopodobnie nie wiesz, co robi większość aplikacji / usług internetowych. Prawdopodobnie myślisz, że całe oprogramowanie to robi:
user do an action
│
v
application start processing action
└──> loop ...
└──> busy processing
end loop
└──> send result to user
Jednak nie tak działają aplikacje internetowe, a nawet każda aplikacja z bazą danych jako zapleczem. Aplikacje internetowe to robią:
user do an action
│
v
application start processing action
└──> make database request
└──> do nothing until request completes
request complete
└──> send result to user
W tym scenariuszu oprogramowanie spędza większość czasu działania, używając 0% czasu procesora, czekając na powrót bazy danych.
Wielowątkowa aplikacja sieciowa:
Wielowątkowe aplikacje sieciowe obsługują powyższe obciążenie w następujący sposób:
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
Wątek spędza więc większość czasu, używając 0% CPU, czekając, aż baza danych zwróci dane. W tym czasie musieli przydzielić pamięć wymaganą dla wątku, który zawiera całkowicie osobny stos programów dla każdego wątku itp. Ponadto musieliby uruchomić wątek, który, choć nie jest tak drogi jak rozpoczęcie pełnego procesu, wciąż nie jest dokładnie tani.
Pętla zdarzeń singlethreaded
Ponieważ spędzamy większość czasu na wykorzystaniu 0% procesora, dlaczego nie uruchomić kodu, gdy nie korzystamy z procesora? W ten sposób każde żądanie będzie nadal otrzymywało tyle samo czasu procesora, co aplikacje wielowątkowe, ale nie musimy zaczynać wątku. Robimy to:
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
W praktyce oba podejścia zwracają dane z mniej więcej takim samym opóźnieniem, ponieważ czas odpowiedzi bazy danych dominuje w przetwarzaniu.
Główną zaletą jest to, że nie musimy odradzać nowego wątku, więc nie musimy robić wielu malloców, które by nas spowolniły.
Magiczne, niewidoczne nawlekanie
Z pozoru tajemniczą rzeczą jest to, jak oba powyższe podejścia radzą sobie z obciążeniem w trybie „równoległym”? Odpowiedź jest taka, że baza danych jest podzielona na wątki. Nasza aplikacja jednowątkowa wykorzystuje tak naprawdę wielowątkowe zachowanie innego procesu: bazy danych.
Tam, gdzie podejście z pojedynczym wątkiem zawodzi
Aplikacja z pojedynczym wątkiem zawiedzie, jeśli musisz zwrócić dużo obliczeń procesora przed zwróceniem danych. Nie mam na myśli pętli for przetwarzającej wynik bazy danych. To wciąż głównie O (n). Chodzi mi o rzeczy takie jak transformacja Fouriera (na przykład kodowanie mp3), ray tracing (renderowanie 3D) itp.
Kolejną pułapką pojedynczych aplikacji jest to, że będą one wykorzystywać tylko jeden rdzeń procesora. Więc jeśli masz serwer czterordzeniowy (co nie jest dzisiaj rzadkością), nie używasz pozostałych 3 rdzeni.
W przypadku niepowodzenia podejścia wielowątkowego
Aplikacja wielowątkowa zawiedzie, jeśli trzeba przydzielić dużo pamięci RAM na wątek. Po pierwsze, użycie pamięci RAM oznacza, że nie można obsłużyć tylu żądań, co aplikacja z pojedynczym wątkiem. Gorzej, Malloc jest powolny. Przydzielanie wielu obiektów (co jest typowe dla współczesnych frameworków internetowych) oznacza, że możemy potencjalnie być wolniejsi niż aplikacje z jednym wątkiem. To tutaj zwykle wygrywa node.js.
Jednym z przypadków użycia, które mogą pogorszyć wielowątkowość, jest potrzeba uruchomienia innego języka skryptowego w wątku. Najpierw zazwyczaj musisz wykonać malloc cały środowisko wykonawcze dla tego języka, a następnie malloc zmienne używane przez skrypt.
Więc jeśli piszesz aplikacje sieciowe w C, go lub java, to narzuty związane z wątkami zwykle nie będą takie złe. Jeśli piszesz serwer WWW C do obsługi PHP lub Ruby, bardzo łatwo jest napisać szybszy serwer w javascript, Ruby lub Python.
Podejście hybrydowe
Niektóre serwery WWW stosują podejście hybrydowe. Nginx i Apache2 na przykład implementują kod przetwarzania sieci jako pulę wątków pętli zdarzeń. Każdy wątek uruchamia pętlę zdarzeń jednocześnie przetwarzając żądania jednowątkowe, ale żądania są równoważone obciążeniem między wieloma wątkami.
Niektóre architektury jednowątkowe również wykorzystują podejście hybrydowe. Zamiast uruchamiania wielu wątków z jednego procesu można uruchomić wiele aplikacji - na przykład 4 serwery node.js na czterordzeniowym komputerze. Następnie używasz modułu równoważenia obciążenia, aby rozłożyć obciążenie między procesami.
W efekcie oba podejścia są technicznie identycznymi odbiciami lustrzanymi.