
Uvod
(jednopotni ) — to je obrazac za pisanje visoko opterećenog softvera, korišten u mnogim popularnim rješenjima:
- …
U ovom članku ćemo razmotriti osnove I/O reagenta i princip njegovog rada, napisati implementaciju u manje od 200 redaka koda i natjerati jednostavni HTTP poslužitelj da obradi više od 40 milijuna zahtjeva/min.
Predgovor
- Članak je napisan s ciljem da pomogne u razumijevanju funkcioniranja I/O reaktora, a time i svesti rizike u njegovoj upotrebi.
- Za razumijevanje članka potrebno je znanje osnova i malo iskustva u razvoju mrežnih aplikacija.
- Sav kod je napisan u programskom jeziku C strogo prema (oprez: dug PDF) za Linux i dostupan je na .
Zašto je to važno?
S porastom popularnosti Interneta, web poslužiteljima je postalo potrebno obraditi velik broj veze istovremeno, zbog čega su isprobana dva pristupa: blokirajuće I/O s velikim brojem OS niti i neblokirajuće I/O u kombinaciji sa sustavom obavijesti o događajima, koji se također naziva „sustavnim selektorom“ (///etc).
Prvi pristup podrazumijeva stvaranje nove OS niti za svaku dolaznu vezu. Njegov nedostatak je loša skalabilnost: operativnom sustavu će biti potrebno izvršiti mnogo i . To su skupe operacije i mogu dovesti do nedostatka slobodne RAM-a pri velikom broju veza.
Modificirana verzija izdava (pool niti), čime se sprječava da sustav nenamjerno prekine izvršavanje, ali unosi novu problematiku: ako u sadašnjem trenutku pool niti blokiraju dugotrajne operacije čitanja, onda drugi soketi koji već mogu primiti podatke neće moći to učiniti.
Drugi pristup koristi (sustavni selektor) koji pruža OS. U ovom članku obradit ćemo najčešći oblik sustavnog selektora, temeljen na obavijestima (događajima, notifikacijama) o spremnosti za I/O operacije, umjesto na . P pojednostavljeni primjer njegove primjene može se predstaviti sljedećim blok-diagramom:

Razlika između ovih pristupa leži u sljedećem:
- Blokirajuće I/O operacije obustavljaju korisnički tok do tada, dok OS ne primanje u tok bajtova (, primanje podataka) ili ne oslobodi dovoljno prostora u internim baferima zapisa za dalje slanje preko (slanje podataka).
- Sistem selektor nakon nekog vremena obaveštava program da je OS već defragmentirao IP pakete (TCP, primanje podataka) ili da je dovoljno prostora u internim baferima zapisa već dostupno (slanje podataka).
Ukratko, rezervisanje toka OS za svaku I/O - to je bespotrebno trošenje računske moći, jer zapravo, tokovi nisu zauzeti korisnim radom (otuda termin ). Sistem selektor rešava ovaj problem, omogućavajući korisničkom programu da efikasnije troši CPU resurse.
Model I/O reaktora
I/O reaktor deluje kao sloj između sistemskog selektora i korisničkog koda. Način na koji funkcioniše opisan je sledećom blok dijagramom:

- Podsećam da je događaj — obaveštenje da je određeni soket u stanju da izvrši neblokirajuću I/O operaciju.
- Obradnik događaja je funkcija koja se poziva od strane I/O reaktora prilikom prijema događaja, koja zatim izvršava neblokirajuću I/O operaciju.
Važno je napomenuti da je I/O reaktor po definiciji jednonitni, ali ništa ne sprečava korišćenje koncepta u višenednom okruženju u odnosu 1 nit: 1 reaktor, čime se optimizuje korišćenje svih CPU jezgara.
Implementacija
Javni interfejs ćemo staviti u datoteku , a implementaciju — u . reactor.h sastojiće se od sledećih deklaracija:
Prikazati deklaracije u reactor.h
typedef struct reactor Reactor;
/*
* Pokazatelj na funkciju koja će biti pozvana od strane I/O reaktora prilikom
* dolaska događaja iz sistemskog selektora.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Vraća `NULL` u slučaju greške, ne-`NULL` pokazatelj na `Reactor` u
* suprotnom.
*/
Reactor *reactor_new(void);
/*
* Oslobađa sistemski selektor, sve registrovane sokete u tom trenutku
* i sam I/O reaktor.
*
* Sledeće funkcije vraćaju -1 u slučaju greške, 0 u slučaju uspeha.
*/
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);
/*
* Pokreće petlju događaja sa vremenskim odbrojavanjem `timeout`.
*
* Ova funkcija će preneti kontrolu pozivnom kodu ako je vreme isteklo
* ili/ili u slučaju nedostatka registrovanih soketa.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Struktura I/O reaktora sastoji se od selektora i , koja svaki soket mapira na CallbackData (struktura izdržava događaja i korisničkog argumenta za njega).
Prikazati Reactor i CallbackData
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Obratite pažnju da koristimo mogućnost da radimo sa preko pokazatelja. U reactor.h deklariramo strukturu reactor, a u reactor.c njoj definišemo, time ne dopuštajući korisniku da izmenjuje njene polja. Ovo je jedan od obrazaca , koji se stilski uklapa u semantiku C-a.
Funkcije reactor_register, reactor_deregister i reactor_reregister ažuriraju listu interesantnih soketa i odgovarajuće obradnike događaja u sistemskom selektoru i u heš tabeli.
Prikazati funkcije registracije
#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;
}Nakon što je I/O reaktor uhvatio događaj sa deskriptorom fd, poziva odgovarajući obradnik događaja, u koji prosleđuje fd, generisanih događaja i korisnički pokazatelj na void.
Prikazati funkciju 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) {
// Greška
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Vrijeme je isteklo
case 0:
result = 0;
goto cleanup;
// Uspješna operacija
default:
// Pozvati obradnike događaja
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Zaključno, niz poziva funkcija u korisničkom kodu izgleda ovako:

Jednoprocesorski server
Kako bismo testirali I/O reaktor pod visokim opterećenjem, napisat ćemo jednostavni HTTP web server koji odgovara slikom na svaki zahtjev.
Kratki vodič o HTTP protokolu
— to je protokol , koji se većinom koristi za interakciju servera s preglednikom.
HTTP se lako može koristiti preko protokola , šaljajući i primajući poruke u formatu koji je određen .
Format zahtjeva
CRLF
CRLF
CRLF
CRLF CRLF
CRLF— to je niz od dva znaka:rin, koji razdvaja prvu liniju zahtjeva, zaglavlja i podatke.<КОМАНДА>— jedna odCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Preglednik će našem serveru slati komanduGET, što znači „Pošalji mi sadržaj datoteke“.<URI>— . Na primjer, ako je URI =/index.html, klijent traži početnu stranicu web sajta.<ВЕРСИЯ HTTP>— verzija HTTP protokola u formatuHTTP/X.Y. Najčešće korištena verzija danas jeHTTP/1.1.<ЗАГОЛОВОК N>— to je par ključ-vrijednost u formatu:, poslan serveru za daljnju analizu.— podaci potrebni serveru za izvršenje operacije. Često je to samo ili bilo koji drugi format.
Format odgovora
CRLF
CRLF
CRLF
CRLF CRLF
<КОД СТАТУСА>— to je broj koji predstavlja rezultat operacije. Naš poslužitelj uvijek će vraćati status 200 (uspješna operacija).<ОПИСАНИЕ СТАТУСА>— stringovno predstavljanje koda statusa. Za kod statusa 200 — to jeOK.<ЗАГОЛОВОК N>— oznaka istog formata kao u zahtjevu. Vraćat ćemo zaglavljaContent-Length(veličina datoteke) iContent-Type: text/html(tip vraćenih podataka).— podaci koje korisnik traži. U našem slučaju to je put do slike u .
Datoteka (jednopotni poslužitelj) uključuje datoteku , koja sadržava sljedeće prototipove funkcija:
Prikaži prototipove funkcija u 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);Također je opisan funkcionalni makro SAFE_CALL() i definirana je funkcija fail(). Makro uspoređuje vrijednost izraza s greškom, a ako je uvjet ispunjen, poziva funkciju fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funkcija fail() ispisuje proslijeđene argumente u terminal (kao ) i završava program s kodom 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);
}Funkcija new_server() vraća deskriptor datoteke 'poslužiteljskog' soketa, koji je stvoren sustavskim pozivima , i i sposoban prihvatiti dolazne veze u neblokirajućem načinu.
Prikaži funkciju 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;
}- Napomena: soket se prvotno stvara u neblokirajućem načinu s pomoću zastavice
SOCK_NONBLOCK, tako da funkcijaon_accept()(nastavite čitati) sustavni pozivaccept()ne zaustavi izvršavanje niti. - Ako
reuse_portjetrue, tada će ova funkcija konfigurirati soket s opcijom pomoću , kako bi koristila istu portu u višepotnoj sredini (vidi odjeljak 'Višepotni poslužitelj').
Obraditelj događaja on_accept() poziva se nakon što OS generira događaj EPOLLIN, što u ovom slučaju znači da nova veza može biti prihvaćena. on_accept() prihvaća novu vezu, prebacuje je u neblokirajući način i registrira s obraditeljem događaja on_recv() u I/O reaktoru.
Prikaži funkciju 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);
}Obraditelj događaja on_recv() poziva se nakon što OS generira događaj EPOLLIN, što u ovom slučaju znači da je veza registrovana on_accept(), spremna za prijem podataka.
on_recv() čita podatke iz veze sve dok HTTP zahtev ne bude potpuno primljen, zatim registruje handler on_send() za slanje HTTP odgovora. Ako klijent prekinu vezu, socket se deregistruje i zatvara putem .
Prikaži funkciju on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Prihvatamo ulazne podatke sve dok recv ne vrati 0 ili grešku
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// Klijent je prekinu vezu
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read je vratio grešku različitu od greške koja blokira poziv
// niti
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Primljen je kompletan HTTP zahtev od klijenta. Sada registrujemo handler
// događaja za slanje podataka
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}Obraditelj događaja on_send() poziva se nakon što OS generira događaj EPOLLOUT, što znači da je veza registrovana on_recv(), spremna za slanje podataka. Ova funkcija šalje HTTP odgovor, koji sadrži HTML sa slikom, klijentu i zatim ponovo menja handler za događaj na on_recv().
Prikaži funkciju on_send()
staticka 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 na kraju, u datoteci http_server.c, u funkciji main() kreiramo I/O reaktora pomoću reactor_new(), kreiramo serverski socket i registrujemo ga, pokrećemo reaktora pomoću reactor_run() tako dugo na jednu minute, a zatim oslobađamo resurse i izlazimo iz programa.
Prikaži 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);
}Provjeravamo da li sve radi kako treba. Kompajliramo (chmod a+x compile.sh && ./compile.sh u korijenu projekta) i pokrećemo server, otvaramo u pretraživaču i promatramo ono što smo očekivali:

Merenje performansi
Prikaži specifikacije mog računara
$ 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
.MMMMMMMMMMMMMMMMMMMIzmjerimo performanse jednog servera s jednim procesom. Otvorimo dva terminala: u jednom pokrenimo ./http_server, u drugom — . Nakon minute, u drugom terminalu će se prikazati sljedeća statistika:
$ 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.19MBNaš server s jednim procesom mogao je obraditi više od 11 milijuna zahtjeva u minuti iz 100 veza. Dobri rezultati, ali možemo li ih poboljšati?
Višenitni server
Kao što je ranije rečeno, I/O reaktor se može kreirati u odvojenim nitima, čime se koriste svi jezgre CPU-a. Primijenimo ovaj pristup u praksi:
Prikaži 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);
}
}Sada svaka nit reaktor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Obratite pažnju na to da je argument funkcije new_server() - to znači da dodjeljujemo poslužiteljskom soketu opciju true, kako bismo ga koristili u višenitnom okruženju. Više informacija možete pronaći Drugi pokušaj .
Sada ćemo mjeriti performanse višenitnog servera:
$ 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.14MB
Broj obrađenih zahtjeva u minuti porastao je oko 3.28 puta! Ali nedostajalo je samo ~dva milijuna do zaokruženog broja, pokušajmo to ispraviti.Prvo ćemo pogledati statistiku generiranu
perf :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses .\/http_server_multithreaded
Statistika performanse za '.\/http_server_multithreaded':
242446,314933 task-clock (msec) # 4,000 CPU-a iskorišteno
1 813 074 kontekst-prebacivanja # 0,007 M\/sec
4 689 migracije CPU-a # 0,019 K\/sec
254 strani-uspjesi # 0,001 K\/sec
895 324 830 170 ciklusi # 3,693 GHz
621 378 066 808 instrukcije # 0,69 insn po ciklusu
119 926 709 370 grane # 494,653 M\/sec
3 227 095 669 promašaji grana # 2,69% svih grana
808 664 promašaji keša
60,604330670 sekundi vrijeme proteklo, kompilacija s -march=native, , povećanje broja pogodaka u , povećanje MAX_EVENTS i korištenje EPOLLET nije donijelo značajnog poboljšanja u performansama. No, što će se dogoditi ako povećamo broj istodobnih konekcija?
Statistika s 352 istodobna konekcije:
$ 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"
Izvođenje testa od 1m @ http:\/\/127.0.0.1:18470
8 niti i 352 konekcije
Thread Stats Avg Stdev Max +\/ Stdev
Latencija 2.12ms 3.79ms 68.23ms 87.49%
Req\/Sec 83.78k 12.69k 169.81k 83.59%
40006142 zahtjeva u 1.00m, 5.48GB pročitano
Zahtjevi\/sec: 665789.26
Transfer\/sec: 93.34MBŽeljeni rezultat je postignut, a uz to i zanimljiva grafika koja prikazuje ovisnost broja obrađenih zahtjeva u 1 minuti od broja konekcija:

Vidimo da nakon nekoliko stotina konekcija broj obrađenih zahtjeva obaju servera naglo opada (kod višedretvenog varijante to je više uočljivo). Je li to povezano s implementacijom TCP\/IP stoga Linuxa? Slobodno ostavite svoje pretpostavke o ovom ponašanju grafika i optimizacijama višedretvenih i jednedretvenih varijanti u komentarima.
Kako u komentarima, ovaj test performansi ne prikazuje ponašanje I\/O reaktora pod stvarnim opterećenjima, jer gotovo uvijek server komunicira s bazom podataka, ispisuje logove, koristi kriptografiju s itd., zbog čega opterećenje postaje nejednako (dinamično). Testovi zajedno sa vanjskim komponentama bit će provedeni u članku o I\/O reaktoru.
Nedostaci I\/O reaktora
Trebate razumjeti da I\/O reaktor nema nedostataka, a to se posebno odnosi na:
- Korištenje I\/O reaktora u višedretvenom okruženju je nešto složenije, jer će biti potrebno ručno upravljati nitima.
- Praksa pokazuje da je u većini slučajeva opterećenje neujednačeno, što može dovesti do toga da jedan tok radi dok je drugi zauzet poslom.
- Ako jedan obrtnik događaja blokira tok, također će se blokirati i sam sistemski selektor, što može dovesti do teško uočljivih grešaka.
Ove probleme rešava , često imajući planifikator koji ravnomerno raspoređuje opterećenje u grupi tokova, a takođe ima i jednostavnije API. O tome će biti reči kasnije, u mom drugom članku.
Zaključak
Time je naše putovanje od teorije do izlaza profajlera završeno.
Ne treba se ovde zaustavljati, jer postoje mnogi drugi interesantni pristupi za pisanje mrežnog softvera s različitim nivoima praktičnosti i brzine. Interesantne, po mom mišljenju, poveznice su navedene ispod.
Do narednog susreta!
Interesantni projekti
- C
Šta još pročitati?
Izvor: habr.com
