
Introducere
(single-threaded ) — este un model pentru scrierea de software de înaltă performanță, utilizat în multe soluții populare:
- …
În acest articol, vom explora fundamentele reactorului I/O și principiul său de funcționare, vom scrie o implementare în mai puțin de 200 de linii de cod și vom face un server HTTP simplu să proceseze peste 40 de milioane de cereri pe minut.
Prefață
- Articolul este scris cu scopul de a ajuta la înțelegerea funcționării reactorului I/O, ceea ce înseamnă și conștientizarea riscurilor asociate utilizării acestuia.
- Pentru a înțelege articolul, este necesar să aveți cunoștințe de bază și o mică experiență în dezvoltarea aplicațiilor de rețea.
- Întregul cod este scris în limbajul C strict conformatenție: PDF lung) pentru Linux și este disponibil pe .
De ce este necesar?
Odată cu creșterea popularității internetului, serverele web au început să fie nevoite să proceseze un număr mare de conexiuni simultan, motiv pentru care au fost testate două abordări: I/O blocant pe un număr mare de fire de execuție OS și I/O non-blocant în combinație cu un sistem de notificare a evenimentelor, cunoscut și sub numele de 'selector de sistem' (///etc).
Prima abordare presupunea crearea unui nou fir de execuție OS pentru fiecare conexiune de intrare. Dezavantajul său este scalabilitatea slabă: sistemul de operare va trebui să efectueze numeroase și . Acestea sunt operații costisitoare și pot conduce la epuizarea memoriei RAM disponibile în cazul unui număr mare de conexiuni.
Versiunea modificată alocă (pool de fire), astfel nepermitând sistemului să întrerupă execuția, dar în același timp aducând o nouă problemă: dacă în acel moment pool-ul de fire blochează operațiuni de citire ce durează, celelalte socket-uri care sunt deja pregătite să primească date nu vor putea să facă acest lucru.
A doua abordare utilizează (selector de sistem) pe care îl furnizează OS. În acest articol, vom analiza cel mai frecvent tip de selector de sistem, bazat pe notificări (evenimente, alerte) privind disponibilitatea operațiunilor I/O, mai degrabă decât pe . Un exemplu simplificat al utilizării sale poate fi reprezentat prin următoarea diagramă de flux:

Diferența dintre cele două abordări este următoarea:
- Operațiile I/O blocante suspendă firul de utilizator până când, până când OS-ul nu pachetele IP , primire de date) sau nu se va elibera un spațiu suficient în bufferele interne de scriere pentru a putea trimite(trimitere de date). Selectorul de sistem
- după un timp informează programul că OS-ul a defragmentat pachetele IP (TCP, primire de date) sau că există suficient spațiu în bufferele interne de scriere este deja disponibil (trimitere de date). este deja În concluzie, rezervarea unui flux OS pentru fiecare I/O este o risipă de putere de calcul, deoarece, de fapt, fluxurile nu sunt ocupate cu muncă utilă (de aici provine termenul
„interupere programatică” Modelul I/O reactor
I/O reactorul acționează ca un strat între selectorul de sistem și codul utilizator. Principiul său de funcționare este descris în următoarea diagrama de flux:
Amintesc că un eveniment este o notificare că un anumit socket este pregătit să execute o operație I/O non-blocantă.

- Handler-ul de evenimente este o funcție apelată de I/O reactor la primirea unui eveniment, care ulterior efectuează o operație I/O non-blocantă.
- Este important de menționat că I/O reactorul este, prin definiție, unitar, dar nimic nu împiedică utilizarea conceptului într-un mediu multi-threaded în raport cu 1 fir: 1 reactor, astfel utilizând toate nucleele CPU.
Interfața publică o vom plasa în fișierul
Implementarea
reactor.h reactor.c . , iar implementarea în Afișați declarațiile în reactor.h
Afișează anunțurile în reactor.h
typedef struct reactor Reactor;
/*
* Pointeur vers la fonction qui sera appelée par le réacteur I/O lorsqu'un
* événement est reçu du sélecteur système.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Retourne `NULL` en cas d'erreur, un pointeur non `NULL` vers `Reactor` autrement.
*/
Reactor *reactor_new(void);
/*
* Libère le sélecteur système, tous les sockets enregistrés à ce moment
* et le réacteur I/O lui-même.
*
* Les fonctions suivantes retournent -1 en cas d'erreur, 0 en cas de succès.
*/
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);
/*
* Démarre la boucle d'événements avec un délai `timeout`.
*
* Cette fonction passera le contrôle au code appelant si le temps imparti est écoulé
* ou/et en l'absence de sockets enregistrés.
*/
int reactor_run(const Reactor *reactor, time_t timeout);La structure du réacteur I/O se compose de sélecteur și , qui associe chaque socket à CallbackData (structure contenant le gestionnaire d'événements et l'argument utilisateur pour celui-ci).
Afficher Reactor et CallbackData
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Veuillez noter que nous avons utilisé la possibilité de gérer un par pointeur. Dans , iar implementarea în nous déclarons la structure reactor, iar în va consta din următoarele declarații: nous la définissons, empêchant ainsi l'utilisateur de modifier explicitement ses champs. C'est l'un des modèles , s'intégrant élégamment dans la sémantique du C.
Functions reactor_register, reactor_deregister și reactor_reregister mettent à jour la liste des sockets d'intérêt et des gestionnaires d'événements correspondants dans le sélecteur système et dans la table de hachage.
Afficher les fonctions d'enregistrement
#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;
}Après que le réacteur I/O ait intercepté un événement avec le descripteur fd, il appelle le gestionnaire d'événements correspondant, lui transmettant fd, des événements générés et un pointeur utilisateur vers void.
Afficher la fonction 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) {
// Eroare
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Timp expirat
case 0:
result = 0;
goto cleanup;
// Operație reușită
default:
// Apelare handleri de evenimente
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}În concluzie, lanțul de apeluri de funcții din codul utilizatorului va arăta astfel:

Server unitar
Pentru a testa reactorul I/O sub o sarcină mare, vom scrie un server HTTP simplu care va răspunde cu o imagine la orice cerere.
Scurt rezumat privind protocolul HTTP
— este un protocol , folosit în principal pentru interacțiunea între server și browser.
HTTP poate fi folosit cu ușurință deasupra protocolului , trimițând și primind mesaje în formatul definit .
Formatul cererii
CRLF
CRLF
CRLF
CRLF CRLFCRLF— este o secvență de două caractere:rșin, care separă prima linie a cererii, capetele de titlu și datele.<КОМАНДА>— este una dintreCONNECT,DELETE,metoda GET.,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Browserul va trimite serverului nostru comandametoda GET., care înseamnă „Trimite-mi conținutul fișierului”.<URI>— . De exemplu, dacă URI =/index.html, atunci clientul cere pagina principală a site-ului.<ВЕРСИЯ HTTP>— versiunea protocolului HTTP în formatulHTTP/X.Y. Cea mai utilizată versiune la ora actuală esteHTTP/1.1.<ЗАГОЛОВОК N>— este o pereche cheie-valoare în formatul:, trimisă serverului pentru o analiză ulterioară.<ДАННЫЕ>— datele necesare serverului pentru a executa operația. De obicei, aceasta este doar sau orice alt format.
Formatul răspunsului
CRLF
CRLF
CRLF
CRLF CRLF<КОД СТАТУСА>— este un număr care reprezintă rezultatul unei operațiuni. Serverul nostru va returna întotdeauna un status 200 (operațiune reușită).<ОПИСАНИЕ СТАТУСА>— o reprezentare sub formă de șir a codului de status. Pentru codul de status 200 — aceasta esteOK.<ЗАГОЛОВОК N>— un antet de același format ca și în cerere. Vom returna anteteleContent-Length(dimensiunea fișierului) șiContent-Type: text/html(tipul datelor returnate).<ДАННЫЕ>— datele solicitate de utilizator. În cazul nostru, aceasta este calea către imagine în .
Fișier (server unitar) include fișierul , care conține următoarele prototipuri de funcții:
Afișați prototipurile de funcții din 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);De asemenea, este descris un macro funcțional SAFE_CALL() și este definită funcția fail(). Macro-ul compară valoarea expresiei cu o eroare, iar dacă condiția s-a îndeplinit, apelează funcția fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funcția fail() tipărește argumentele transmise în terminal (ca și ) și finalizează programul cu codul 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);
}Funcția new_server() returnează un descriptor de fișier pentru socket-ul „server”, creat prin apelurile de sistem , și și capabil să accepte conexiuni incoming în mod non-blocant.
Afișați funcția 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;
}- Rețineți că socketul este creat inițial în mod non-blocant folosind flag-ul
SOCK_NONBLOCK, astfel încât în funcțiaon_accept()(citiți mai departe) apelul de sistemaccept()nu oprește execuția firului. - Dacă
reuse_portestetrue, atunci această funcție va configura socketul cu opțiunea prin , pentru a folosi aceeași port într-un mediu multi-fir (vezi secțiunea „Server multi-fir”).
Handler-ul de evenimente on_accept() este apelat după ce sistemul de operare generează un eveniment EPOLLIN, care în acest caz înseamnă că o nouă conexiune poate fi acceptată. on_accept() acceptă o nouă conexiune, o comută în mod non-blocant și o înregistrează cu handler-ul de evenimente on_recv() în reactorul I/O.
Afișați funcția 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);
}Handler-ul de evenimente on_recv() este apelat după ce sistemul de operare generează un eveniment EPOLLIN, în acest caz înseamnă că conexiunea înregistrată on_accept(), este pregătită pentru a primi date.
on_recv() citește date din conexiune până când cererea HTTP este complet primită, apoi înregistrează handler-ul on_send() pentru a trimite răspunsul HTTP. Dacă clientul a încheiat conexiunea, socket-ul este deregistrat și închis prin .
Afișează funcția on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
\/\/ Primim datele de intrare până când recv returnează 0 sau o eroare
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
\/\/ Clientul a încheiat conexiunea
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
\/\/ read a returnat o eroare, diferită de eroarea care ar bloca
\/\/ firul
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
\/\/ A fost primit un request HTTP complet de la client. Acum înregistrăm handler-ul
\/\/ evenimentelor pentru a trimite datele
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}Handler-ul de evenimente on_send() este apelat după ce sistemul de operare generează un eveniment EPOLLOUT, înseamnă că conexiunea înregistrată on_recv(), este pregătită pentru a trimite date. Această funcție trimite răspunsul HTTP, conținând HTML cu o imagine, clientului și apoi schimbă din nou handler-ul evenimentului la on_recv().
Afișează funcția 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);
}Și, în cele din urmă, în fișierul http_server.c, în funcția main() creăm un reactor I/O prin reactor_new(), creăm un socket de server și îl înregistrăm, apoi pornim reactorul cu ajutorul reactor_run() timp de exact un minut, după care eliberăm resursele și ieșim din program.
Afișează 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);
}Să verificăm că totul funcționează corect. Compilăm (chmod a+x compile.sh && .\/compile.sh în rădăcina proiectului) și lansăm server-ul personalizat, deschidem în browser și observăm ceea ce ne așteptam:

Măsurarea performanței
Afișează specificațiile mașinii mele
$ 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
.MMMMMMMMMMMMMMMMMMMMăsurăm performanța unui server pe un singur fir. Vom deschide două terminale: în unul vom rula ./http_server, în celălalt — . După un minut, în cel de-al doilea terminal va apărea următoarea statistică:
$ 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.19MBServerul nostru pe un singur fir a procesat peste 11 milioane de cereri pe minut, provenind din 100 de conexiuni. Rezultat decent, dar se poate îmbunătăți?
Server pe mai multe fire
Așa cum s-a menționat anterior, reactorul I/O poate fi creat în fire separate, astfel utilizând toate nuclee CPU-ului. Să aplicăm această abordare în practică:
Arată 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);
}
}Acum fiecare fir reactor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Observați că argumentul funcției new_server() sunt susținute de true. Asta înseamnă că atribuim socket-ului serverului opțiunea , pentru a-l folosi într-un mediu multi-thread. Puteți citi mai multe detalii .
Al doilea încercare
Acum să măsurăm performanța serverului pe mai multe fire:
$ 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.14MBNumărul de cereri procesate pe minut a crescut de aproximativ 3.28 ori! Dar pentru a ajunge la un număr rotund ne-au lipsit doar aproximativ două milioane, să încercăm să corectăm asta.
Mai întâi să ne uităm la statistica generată :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses .\/http_server_multithreaded
Statistici ale contracontorului pentru '.\/http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPU-uri utilizate
1 813 074 schimbări de context # 0,007 M/sec
4 689 migrații CPU # 0,019 K/sec
254 erori de pagină # 0,001 K/sec
895 324 830 170 cicluri # 3,693 GHz
621 378 066 808 instrucțiuni # 0,69 insn per ciclu
119 926 709 370 ramuri # 494,653 M/sec
3 227 095 669 pierderi de ramuri # 2,69% din toate ramurile
808 664 pierderi de cache
60,604330670 secunde timp scurs, compilare cu -march=native, , creșterea numărului de lovituri în , creșterea MAX_EVENTS și utilizarea EPOLLET nu a adus un câștig semnificativ în performanță. Dar ce se întâmplă dacă creștem numărul de conexiuni simultane?
Statistici cu 352 de conexiuni simultane:
$ 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"
Se execută un test de 1m @ http:\/\/127.0.0.1:18470
8 fire de execuție și 352 de conexiuni
Statistici fir Avg Stdev Max +\/ - Stdev
Viteză de răspuns 2.12ms 3.79ms 68.23ms 87.49%
Req\/Sec 83.78k 12.69k 169.81k 83.59%
40006142 cereri în 1.00m, 5.48GB citite
Cereri\/sec: 665789.26
Transfer\/sec: 93.34MBRezultatul dorit a fost obținut, iar împreună cu el și un grafic interesant care arată dependența numărului de cereri procesate într-un minut de numărul de conexiuni:

Vedem că după câteva sute de conexiuni, numărul cererilor procesate scade brusc pentru ambele servere (pentru varianta multithreaded este mai evident). Este aceasta legată de implementarea stivei TCP\/IP în Linux? Împărtășiți-vă părerile despre acest comportament al graficului și optimizările variantelor multithreaded și singlethreaded în comentarii.
Cum în comentarii, acest test de performanță nu arată comportamentul reactorului I\/O în condiții de stres reale, deoarece serverul interacționează aproape întotdeauna cu baza de date, scrie în loguri, folosește criptografie cu etc., ceea ce face ca sarcina să devină heterogenă (dinamică). Testele împreună cu componente terțe vor fi efectuate în articolul despre reactorul I\/O.
Dezavantajele reactorului I\/O
Trebuie înțeles că reactorul I\/O nu este lipsit de dezavantaje, și anume:
- Folosirea reactorului I\/O într-un mediu multithreaded este ceva mai complicată, deoarece va trebui să gestionați manual firele.
- Practică demonstrează că, în majoritatea cazurilor, încărcătura este inegală, ceea ce poate duce la faptul că un fir va fi ocupat în timp ce altul va fi încărcat cu muncă.
- Dacă un handler de eveniment blochează firul, atunci și selectorul sistemului va fi blocat, ceea ce poate duce la bug-uri greu de depistat.
Aceste probleme sunt rezolvate de , adesea cu un planner care distribuie uniform încărcătura în pool-ul de fire și care oferă, de asemenea, o API mai prietenoasă. Despre aceasta voi discuta mai târziu, în alt articol.
Concluzie
Aici s-a încheiat călătoria noastră din teorie direct către ieșirea profiler-ului.
Nu ar trebui să ne oprim aici, deoarece există o mulțime de alte abordări interesante pentru dezvoltarea software-ului de rețea, cu diverse niveluri de confort și viteză. Linkuri interesante, în opinia mea, sunt prezentate mai jos.
Ne vedem data viitoare!
Proiecte interesante
- C
Ce altceva să citim?
Sursa: habr.com
