
Inleiding
(single-threaded ) - dit is een patroon voor het schrijven van hoogbelaste software, gebruikt in veel populaire oplossingen:
- …
In dit artikel zullen we de onderliggende werking van de I/O reactor bespreken, een implementatie schrijven in minder dan 200 regels code en een eenvoudige HTTP-server meer dan 40 miljoen verzoeken per minuut laten verwerken.
Voorwoord
- Dit artikel is geschreven met als doel om te helpen begrijpen hoe de I/O reactor functioneert en dus de risico's van het gebruik ervan te beseffen.
- Voor het begrijpen van dit artikel zijn basiskennis vereist van en enige ervaring met het ontwikkelen van netwerkapplicaties.
- Alle code is geschreven in de programmeertaal C volgens delet op: lange PDF) voor Linux en is beschikbaar op .
Waarom is dit nodig?
Met de groeiende populariteit van het internet moeten webservers tegelijkertijd een groot aantal verbindingen verwerken, waardoor twee benaderingen zijn uitgeprobeerd: blokkerende I/O met een groot aantal OS-threads en niet-blokkerende I/O in combinatie met een systeem voor gebeurteniswaarschuwing, ook wel 'system selector' genoemd (///etc).
De eerste benadering houdt in dat er een nieuwe OS-thread voor elke binnenkomende verbinding wordt aangemaakt. Het nadeel hiervan is dat het slecht schaalbaar is: het besturingssysteem moet veel en . Dit zijn dure operaties en kunnen leiden tot een gebrek aan beschikbare RAM bij een aanzienlijk aantal verbindingen.
De gemodificeerde versie wijst (thread pool), waardoor het systeem voorkomt dat het proces abrupt wordt beëindigd, maar dit brengt een nieuw probleem met zich mee: als op dat moment de thread pool langdurige leesoperaties blokkeert, kunnen andere sockets die al in staat zijn om gegevens te ontvangen dit niet doen.
De tweede benadering gebruikt (system selector) dat door het OS wordt aangeboden. In dit artikel wordt de meest voorkomende vorm van een system selector besproken, die is gebaseerd op gebeurtenissen (meldingen) over gereedheid voor I/O operaties, in plaats van Een vereenvoudigd voorbeeld van het gebruik ervan kan worden weergegeven met de volgende blokdiagram:

Het verschil tussen deze benaderingen is als volgt:
- Blokkerende I/O-operaties onderbreken de gebruikersstroom totdathet besturingssysteem correct de binnenkomende in een bytestraam (, gegevensontvangst) of er voldoende ruimte vrijgemaakt is in de interne schrijfbuffers voor verdere verzending via (gegevensverzending).
- De systeemselector meldt op een gegeven moment de applicatie dat het besturingssysteem al de IP-pakketten heeft gedefragmenteerd (TCP, gegevensontvangst) of dat er voldoende ruimte in de interne schrijfbuffers al beschikbaar is (gegevensverzending).
Samenvattend, het reserveren van een besturingssysteemstroom voor elke I/O is een verspilling van rekenkracht, want in werkelijkheid zijn de stromen niet bezig met nuttig werk (hieruit komt de term ). De systeemselector lost dit probleem op door de gebruikersapplicatie CPU-bronnen veel efficiënter te laten gebruiken.
Het I/O-reactormodel
De I/O-reactor fungeert als een laag tussen de systeemselector en de gebruikerscode. Het principe van zijn werking wordt beschreven door het volgende stroomdiagram:

- Ik herinner jullie eraan dat een gebeurtenis een notificatie is dat een bepaalde socket in staat is om een non-blocking I/O-operatie uit te voeren.
- De gebeurtenisbehandelaar is een functie die door de I/O-reactor wordt aangeroepen bij het ontvangen van een gebeurtenis, die vervolgens een non-blocking I/O-operatie uitvoert.
Het is belangrijk op te merken dat de I/O-reactor per definitie single-threaded is, maar er staat niets in de weg om het concept te gebruiken in een multi-threaded omgeving in een 1 thread: 1 reactor verhouding, waarmee alle CPU-kernen worden benut.
Implementatie
We plaatsen de publieke interface in het bestand , en de implementatie in . reactor.h zal uit de volgende declaraties bestaan:
Toon de declaraties in reactor.h
typedef struct reactor Reactor;
/*
* Een pointer naar de functie die door de I/O reactor zal worden aangeroepen wanneer
* er een gebeurtenis van de systeemselecteur binnenkomt.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Geeft `NULL` terug in geval van een fout, een niet-`NULL` pointer naar `Reactor` in
* andere gevallen.
*/
Reactor *reactor_new(void);
/*
* Vrijgeven van de systeemselecteur, alle geregistreerde sockets op dat moment
* en zelf de I/O reactor.
*
* De volgende functies geven -1 terug in geval van een fout, 0 in geval van succes.
*/
int reactor_destroy(Reactor *reactor);
int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg);
int reactor_deregister(const Reactor *reactor, int fd);
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg);
/*
* Start de evenementenlus met een time-out van `timeout`.
*
* Deze functie geeft de controle terug aan de aanroepende code als de toegewezen tijd is verstreken
* of/én als er geen geregistreerde sockets zijn.
*/
int reactor_run(const Reactor *reactor, time_t timeout);De structuur van de I/O reactor bestaat uit een selector en , die elke socket koppelt aan CallbackData (een structuur van de gebeurtenishandler en de gebruikersargumenten daarvoor).
Toon Reactor en CallbackData
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Let op dat we de mogelijkheid hebben gebruikt om om te gaan met door middel van pointers. In reactor.h declareren we de structuur reactor, en in reactor.c definiëren we deze, zodat de gebruiker niet expliciet de velden kan wijzigen. Dit is een van de patronen , dat elegant past in de semantiek van C.
Functies reactor_register, reactor_deregister en reactor_reregister actualiseren de lijst van relevante sockets en bijbehorende gebeurtenishandlers in de systeemselector en in de hashtabel.
Toon registratiefuncties
#define REACTOR_CTL(reactor, op, fd, interest)
if (epoll_ctl(reactor->epoll_fd, op, fd,
&(struct epoll_event){.events = interest,
.data = {.fd = fd}}) == -1) {
perror("epoll_ctl");
return -1;
}
int reactor_register(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg) {
REACTOR_CTL(reactor, EPOLL_CTL_ADD, fd, interest)
g_hash_table_insert(reactor->table, int_in_heap(fd),
callback_data_new(callback, callback_arg));
return 0;
}
int reactor_deregister(const Reactor *reactor, int fd) {
REACTOR_CTL(reactor, EPOLL_CTL_DEL, fd, 0)
g_hash_table_remove(reactor->table, &fd);
return 0;
}
int reactor_reregister(const Reactor *reactor, int fd, uint32_t interest,
Callback callback, void *callback_arg) {
REACTOR_CTL(reactor, EPOLL_CTL_MOD, fd, interest)
g_hash_table_insert(reactor->table, int_in_heap(fd),
callback_data_new(callback, callback_arg));
return 0;
}Nadat de I/O reactor een gebeurtenis heeft onderschept met de handle fd, roept hij de overeenkomstige gebeurtenishandler aan, waarin hij fd, van de gegenereerde gebeurtenissen en een gebruikerspointer doorgeeft naar void.
Toon de functie reactor_run()
int reactor_run(const Reactor *reactor, time_t timeout) {
int result;
struct epoll_event *events;
if ((events = calloc(MAX_EVENTS, sizeof(*events))) == NULL)
abort();
time_t start = time(NULL);
while (true) {
time_t passed = time(NULL) - start;
int nfds =
epoll_wait(reactor->epoll_fd, events, MAX_EVENTS, timeout - passed);
switch (nfds) {
// Fout
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Tijd is om
case 0:
result = 0;
goto cleanup;
// Succesvolle operatie
default:
// Roep de gebeurtenishandlers aan
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Samenvattend zal de keten van aanroepen in de gebruikerscode als volgt zijn:

Eén enkele server
Om de I/O reactor onder hoge belasting te testen, schrijven we een eenvoudige HTTP-webserver die op elk verzoek met een afbeelding reageert.
Korte handleiding voor het HTTP-protocol
— is een protocol , dat voornamelijk wordt gebruikt voor de interactie tussen de server en de browser.
HTTP kan gemakkelijk bovenop gebruikt worden, door berichten te verzenden en te ontvangen in een formaat dat is gedefinieerd door de specificatie. .
CRLFCRLFCRLFCRLF CRLF
CRLF— is een sequentie van twee tekens:r, die de eerste regel van het verzoek, de headers en de gegevens scheidt.enn, die de eerste regel van het verzoek, koppen en gegevens scheidt.— is een van deCONNECTDELETE,HEAD,GET,OPTIONS,PATCH,POST,PUT,TRACE,. De browser zal onze server een commando sturen,wat betekent "Stuur me de inhoud van het bestand."GET, wat betekent ‘Stuur me de inhoud van het bestand’.Uniform Resource Identifier— , dan vraagt de cliënt de startpagina van de website aan./index.html, dat de klant de startpagina van de website aanvraagt.— is de versie van het HTTP-protocol in het formaatHTTP/X.Y.De meest gebruikte versie tot nu toe isHTTP/1.1.— is een paar sleutel-waarde in het formaat:, verzonden naar de server voor verdere verwerking.— zijn de gegevens die de server nodig heeft om de operatie uit te voeren. Dit is vaak gewoonJSON Antwoordformaat
CRLFCRLFCRLFCRLF CRLF
CRLF
CRLF
CRLF
CRLF CRLF
<КОД СТАТУСА>— dit is een nummer dat het resultaat van een bewerking vertegenwoordigt. Onze server zal altijd status 200 (succesvolle bewerking) teruggeven.<ОПИСАНИЕ СТАТУСА>— een stringweergave van de statuscode. Voor statuscode 200 is ditOK.— is een paar sleutel-waarde in het formaat— de kop van hetzelfde formaat als de aanvraag. We zullen de koppen teruggevenContent-Length(bestandsgrootte) enContent-Type: text/html(type van de geretourneerde gegevens).— zijn de gegevens die de server nodig heeft om de operatie uit te voeren. Dit is vaak gewoon— de door de gebruiker aangevraagde gegevens. In ons geval is dit het pad naar de afbeelding in .
Bestand (enkelvoudige server) omvat het bestand , dat de volgende functieprototypes bevat:
Toon functieprototypes in common.h
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов принять новое соединение.
*/
static void on_accept(void *arg, int fd, uint32_t events);
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов отправить HTTP ответ.
*/
static void on_send(void *arg, int fd, uint32_t events);
/*
* Обработчик событий, который вызовется после того, как сокет будет
* готов принять часть HTTP запроса.
*/
static void on_recv(void *arg, int fd, uint32_t events);
/*
* Переводит входящее соединение в неблокирующий режим.
*/
static void set_nonblocking(int fd);
/*
* Печатает переданные аргументы в stderr и выходит из процесса с
* кодом `EXIT_FAILURE`.
*/
static noreturn void fail(const char *format, ...);
/*
* Возвращает файловый дескриптор сокета, способного принимать новые
* TCP соединения.
*/
static int new_server(bool reuse_port);Ook is er een functionele macro beschreven SAFE_CALL() en is er een functie gedefinieerd fail(). De macro vergelijkt de uitdrukking met een foutwaarde, en als de voorwaarde waar is, roept deze functie aan fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Functie fail() print de doorgegeven argumenten naar de terminal (zoals ) en beëindigt het programma met code EXIT_FAILURE:
static noreturn void fail(const char *format, ...) {
va_list args;
va_start(args, format);
vfprintf(stderr, format, args);
va_end(args);
fprintf(stderr, ": %sn", strerror(errno));
exit(EXIT_FAILURE);
}Functie new_server() geeft een bestandshandle van de "server" socket terug, gemaakt met systeemaanroepen , en en in staat om binnenkomende verbindingen in niet-blokkerende modus te accepteren.
Toon functie new_server()
static int new_server(bool reuse_port) {
int fd;
SAFE_CALL((fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP)),
-1);
if (reuse_port) {
SAFE_CALL(
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &(int){1}, sizeof(int)),
-1);
}
struct sockaddr_in addr = {.sin_family = AF_INET,
.sin_port = htons(SERVER_PORT),
.sin_addr = {.s_addr = inet_addr(SERVER_IPV4)},
.sin_zero = {0}};
SAFE_CALL(bind(fd, (struct sockaddr *)&addr, sizeof(addr)), -1);
SAFE_CALL(listen(fd, SERVER_BACKLOG), -1);
return fd;
}- Let op dat de socket oorspronkelijk in niet-blokkerende modus wordt aangemaakt met de vlag
SOCK_NONBLOCK, zodat in de functieon_accept()de systeemaanroepaccept()de uitvoering van de thread niet stopt. - Als
reuse_portis gelijk aantrue, dan configureert deze functie de socket met de optie via , om dezelfde poort in een multithread-omgeving te gebruiken (zie sectie "Multithread-server").
De evenementenhandler on_accept() wordt aangeroepen nadat het besturingssysteem een gebeurtenis heeft gegenereerd EPOLLIN, wat in dit geval betekent dat er een nieuwe verbinding kan worden geaccepteerd. on_accept() accepteert een nieuwe verbinding, schakelt deze om naar niet-blokkerende modus en registreert deze met de evenementenhandler on_recv() in de I/O reactor.
Toon de functie on_accept()
static void on_accept(void *arg, int fd, uint32_t events) {
int incoming_conn;
SAFE_CALL((incoming_conn = accept(fd, NULL, NULL)), -1);
set_nonblocking(incoming_conn);
SAFE_CALL(reactor_register(reactor, incoming_conn, EPOLLIN, on_recv,
request_buffer_new()),
-1);
}De evenementenhandler on_recv() wordt aangeroepen nadat het besturingssysteem een gebeurtenis heeft gegenereerd EPOLLIN, wat in dit geval betekent dat de geregistreerde verbinding on_accept(), klaar is om gegevens te ontvangen.
on_recv() leest gegevens uit de verbinding totdat de HTTP-aanroep volledig is ontvangen, waarna het de handler registreert on_send() om het HTTP-antwoord te verzenden. Als de client de verbinding verbreekt, wordt de socket gede-registerd en gesloten met .
Toon de functie on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Ontvangen gegevens totdat recv 0 of een fout retourneert
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// De client heeft de verbinding verbroken
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read retourneerde een fout, anders dan een fout die de aanroep zou blokkeren
// de thread
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Volledige HTTP-aanroep van de client ontvangen. Nu registreren we de handler
// evenementen om gegevens te verzenden
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}De evenementenhandler on_send() wordt aangeroepen nadat het besturingssysteem een gebeurtenis heeft gegenereerd EPOLLOUT, wat betekent dat de geregistreerde verbinding on_recv(), klaar is om gegevens te verzenden. Deze functie verzendt een HTTP-antwoord, inclusief HTML met een afbeelding, naar de client en wijzigt vervolgens de gebeurtenis-handler weer naar on_recv().
Toon de functie on_send()
static void on_send(void *arg, int fd, uint32_t events) {
const char *content = "<img "
"src="https://habrastorage.org/webt/oh/wl/23/"
"ohwl23va3b-dioerobq_mbx4xaw.jpeg">";
char response[1024];
sprintf(response,
"HTTP/1.1 200 OK" CRLF "Content-Length: %zd" CRLF "Content-Type: "
"text/html" DOUBLE_CRLF "%s",
strlen(content), content);
SAFE_CALL(send(fd, response, strlen(response), 0), -1);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLIN, on_recv, arg), -1);
}En tenslotte, in het bestand http_server.c, in de functie main() maken we een I/O-reactor aan met behulp van reactor_new(), maken een server socket aan en registreren deze, starten de reactor met behulp van reactor_run() precies voor één minuut, waarna we de middelen vrijgeven en het programma verlaten.
Toon http_server.c
#include "reactor.h"
static Reactor *reactor;
#include "common.h"
int main(void) {
SAFE_CALL((reactor = reactor_new()), NULL);
SAFE_CALL(
reactor_register(reactor, new_server(false), EPOLLIN, on_accept, NULL),
-1);
SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
SAFE_CALL(reactor_destroy(reactor), -1);
}Laten we controleren of alles goed werkt. We compileren (chmod a+x compile.sh && ./compile.sh in de hoofdmap van het project) en starten de zelfgeschreven server, openen in de browser en zien wat we verwachtten:

Prestatiemetingen
Toon de specificaties van mijn machine
$ screenfetch
MMMMMMMMMMMMMMMMMMMMMMMMMmds+. OS: Mint 19.1 tessa
MMm----::-://///////////oymNMd+` Kernel: x86_64 Linux 4.15.0-20-generic
MMd /++ -sNMd: Uptime: 2h 34m
MMNso/` dMM `.::-. .-::.` .hMN: Packages: 2217
ddddMMh dMM :hNMNMNhNMNMNh: `NMm Shell: bash 4.4.20
NMm dMM .NMN/-+MMM+-/NMN` dMM Resolution: 1920x1080
NMm dMM -MMm `MMM dMM. dMM DE: Cinnamon 4.0.10
NMm dMM -MMm `MMM dMM. dMM WM: Muffin
NMm dMM .mmd `mmm yMM. dMM WM Theme: Mint-Y-Dark (Mint-Y)
NMm dMM` ..` ... ydm. dMM GTK Theme: Mint-Y [GTK2/3]
hMM- +MMd/-------...-:sdds dMM Icon Theme: Mint-Y
-NMm- :hNMNNNmdddddddddy/` dMM Font: Noto Sans 9
-dMNs-``-::::-------.`` dMM CPU: Intel Core i7-6700 @ 8x 4GHz [52.0°C]
`/dMNmy+/:-------------:/yMMM GPU: NV136
./ydNMMMMMMMMMMMMMMMMMMMMM RAM: 2544MiB / 7926MiB
.MMMMMMMMMMMMMMMMMMMLaten we de prestaties van een single-thread server meten. We openen twee terminals: in de ene starten we ./http_server, in de andere — . Na een minuut zal de volgende statistiek in de tweede terminal verschijnen:
$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
8 threads and 100 connections
Thread Stats Avg Stdev Max +/− Stdev
Latency 493.52us 76.70us 17.31ms 89.57%
Req/Sec 24.37k 1.81k 29.34k 68.13%
11657769 requests in 1.00m, 1.60GB read
Requests/sec: 193974.70
Transfer/sec: 27.19MBOnze single-thread server kon meer dan 11 miljoen verzoeken per minuut verwerken, afkomstig van 100 verbindingen. Een goed resultaat, maar kunnen we dit verbeteren?
Multi-thread server
Zoals eerder vermeld, kan de I/O reactor in aparte threads worden gemaakt, waardoor alle CPU-kernen worden benut. Laten we deze aanpak in de praktijk toepassen:
Toon http_server_multithreaded.c
#include "reactor.h"
static Reactor *reactor;
#pragma omp threadprivate(reactor)
#include "common.h"
int main(void) {
#pragma omp parallel
{
SAFE_CALL((reactor = reactor_new()), NULL);
SAFE_CALL(reactor_register(reactor, new_server(true), EPOLLIN,
on_accept, NULL),
-1);
SAFE_CALL(reactor_run(reactor, SERVER_TIMEOUT_MILLIS), -1);
SAFE_CALL(reactor_destroy(reactor), -1);
}
}Nu heeft elke thread reactor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Let op dat het argument van de functie new_server() is true. Dit betekent dat we de server socket de optie geven , zodat deze in een multi-thread omgeving kan worden gebruikt. Meer informatie kunt u lezen .
Tweede ronde
Laten we nu de prestaties van de multi-thread server meten:
$ wrk -c100 -d1m -t8 http://127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Running 1m test @ http://127.0.0.1:18470
8 threads and 100 connections
Thread Stats Avg Stdev Max +/− Stdev
Latency 1.14ms 2.53ms 40.73ms 89.98%
Req/Sec 79.98k 18.07k 154.64k 78.65%
38208400 requests in 1.00m, 5.23GB read
Requests/sec: 635876.41
Transfer/sec: 89.14MBHet aantal verwerkte verzoeken per minuut is met ongeveer 3,28 keer gestegen! Maar we missen nog ongeveer twee miljoen voor een rond getal, laten we dit proberen te corrigeren.
Laten we eerst kijken naar de statistieken die zijn gegenereerd :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses .\/http_server_multithreaded
Prestatiecounterstatistieken voor '.\/http_server_multithreaded':
242446,314933 task-clock (msec) # 4.000 CPU's benut
1 813 074 context-switches # 0,007 M/sec
4 689 cpu-migrations # 0,019 K/sec
254 page-faults # 0,001 K/sec
895 324 830 170 cycles # 3,693 GHz
621 378 066 808 instructions # 0,69 insn per cycle
119 926 709 370 branches # 494,653 M/sec
3 227 095 669 branch-misses # 2,69% van alle branches
808 664 cache-misses
60,604330670 seconden verstreken tijd, compilatie met -march=native, , verhoging van het aantal cache-hits , verhoging MAX_EVENTS en gebruik van EPOLLET heeft geen significante prestatieverbetering opgeleverd. Maar wat gebeurt er als we het aantal gelijktijdige verbindingen verhogen?
Statistieken bij 352 gelijktijdige verbindingen:
$ wrk -c352 -d1m -t8 http:\/\/127.0.0.1:18470 -H "Host: 127.0.0.1:18470" -H "Accept-Language: en-US,en;q=0.5" -H "Connection: keep-alive"
Een test van 1 minuut draaien @ http:\/\/127.0.0.1:18470
8 threads en 352 verbindingen
Threadstatistieken Gem. Stdev Max +\/ - Stdev
Latency 2.12ms 3.79ms 68.23ms 87.49%
Req\/Sec 83.78k 12.69k 169.81k 83.59%
40006142 verzoeken in 1.00m, 5.48GB gelezen
Verzoeken\/sec: 665789.26
Overdracht\/sec: 93.34MBHet gewenste resultaat is behaald, evenals een interessante grafiek die de relatie tussen het aantal verwerkte verzoeken per minuut en het aantal verbindingen laat zien:

We zien dat na een paar honderd verbindingen het aantal verwerkte verzoeken scherp daalt voor beide servers (bij de multithreaded variant is dit duidelijker). Is dit gerelateerd aan de implementatie van de TCP\/IP-stack in Linux? Deel gerust uw gedachten over dit gedrag van de grafiek en optimalisaties van de multithreaded en single-threaded varianten in de reacties.
Zoals in de opmerkingen, toont deze prestatie-test niet het gedrag van de I\/O-reactor onder echte belasting, aangezien de server bijna altijd interactie heeft met de database, logboeken uitvoert, cryptografie gebruikt met enzovoort, waardoor de belasting ongelijkmatig (dynamisch) wordt. Tests samen met externe componenten zullen worden uitgevoerd in het artikel over de I\/O-reactor.
Nadelen van de I\/O-reactor
Het is belangrijk te begrijpen dat de I\/O-reactor niet zonder tekortkomingen is, namelijk:
- Het gebruik van de I\/O-reactor in een multithreaded omgeving is iets moeilijker, omdat je handmatig met de threads moet omgaan.
- Praktijk toont aan dat de belasting in de meeste gevallen ongelijkmatig is, wat ertoe kan leiden dat de ene stroom voorrang krijgt terwijl de andere met werk is belast.
- Als één gebeurtenisbehandelaar een stroom blokkeert, wordt ook de systeemselector geblokkeerd, wat kan leiden tot moeilijk te achterhalen bugs.
Deze problemen worden opgelost door , die vaak een scheduler heeft die de belasting gelijkmatig over de pool van threads verdeelt en bovendien een gebruiksvriendelijker API heeft. Hierover later meer in mijn andere artikel.
Conclusie
Hier eindigt onze reis van theorie naar het profiler-uitstoot.
We moeten hier niet stoppen, want er zijn veel andere interessante benaderingen voor het schrijven van netwerksoftware met verschillende niveaus van gebruiksvriendelijkheid en snelheid. Interessante links, naar mijn mening, zijn hieronder vermeld.
Until we meet again!
Interessante projecten
- C
Wat nog meer te lezen?
Bron: habr.com
