
Úvod
(jednovláknový ) — to je vzor pro psaní vysoce zatíženého softwaru, který se používá v mnoha populárních řešeních:
- …
V tomto článku se podíváme na vnitřnosti I/O reaktoru a princip jeho fungování, napíšeme implementaci v méně než 200 řádcích kódu a přimějeme jednoduchý HTTP server zpracovávat více než 40 milionů požadavků/min.
Úvod
- Článek je napsán s cílem pomoci pochopit fungování I/O reaktoru a tím si uvědomit rizika spojená s jeho používáním.
- Pro pochopení článku je nutné mít znalosti základů a malou zkušenost s vývojem síťových aplikací.
- Veškerý kód je napsán v jazyce C přísně podle (pozor: dlouhý PDF) pro Linux a je dostupný na .
Proč je to potřebné?
S rostoucí popularitou internetu se webovým serverům stalo potřeba zpracovávat velké množství připojení zároveň, proto byly vyzkoušeny dva přístupy: blokující I/O na velkém počtu vláken OS a nebločící I/O v kombinaci se systémem notifikací o událostech, také nazývaným „systémový selektor“ (///etc).
První přístup zahrnoval vytvoření nového vlákna OS pro každé příchozí připojení. Jeho nevýhodou je špatná škálovatelnost: operační systém musí provádět mnoho a . Tyto operace jsou nákladné a mohou vést k nedostatku volné RAM při značném počtu připojení.
Upravená verze určuje (vláknový pool), čímž zabrání systému v havarijním ukončení vykonávání, ale zároveň přináší nový problém: pokud v daném okamžiku vlákna poolu blokují dlouhé operace čtení, tak další sokety, které už mohou přijímat data, to nebudou moci provést.
Druhý přístup používá (systémový selektor), který poskytuje OS. V tomto článku je rozebrán nejčastěji se vyskytující typ systémového selektoru, založený na notifikacích (událostech, upozorněních) o připravenosti k I/O operacím, spíše než na . Zjednodušený příklad jeho použití lze představit následujícím diagramem:

Rozdíl mezi těmito přístupy spočívá v následujícím:
- Blokující I/O operace pozastavují uživatelský tok dokud, dokud operační systém řádně příchozí do proudu bajtů (, příjem dat) nebo se neuvolní dostatek místa ve vnitřních vyrovnávacích pamětích pro následný přenos přes (odesílání dat).
- Systémový selektor po nějaké době informuje program, že operační systém již defragmenoval IP pakety (TCP, příjem dat) nebo že je dostatek místa ve vnitřních vyrovnávacích pamětích již k dispozici (odesílání dat).
Závěrem, rezervace proudu operačního systému pro každý I/O je zbytečná ztráta výpočetní síly, protože ve skutečnosti, proudy nejsou zaplněny užitečnou prací (odtud pochází termín ). Systémový selektor řeší tento problém, což umožňuje uživatelskému programu využívat CPU zdroje mnohem efektivněji.
Model I/O reaktoru
I/O reaktor funguje jako mezivrstva mezi systémovým selektorem a uživatelským kódem. Princip jeho činnosti je popsán následujícím blokovým schématem:

- Připomínám, že událost je upozornění, že určitý soket je schopen provést neblokující I/O operaci.
- Zpracovatel událostí je funkce vyvolaná I/O reaktorem při obdržení události, která poté vykonává neblokující I/O operaci.
Je důležité poznamenat, že I/O reaktor je podle definice jednovláknový, ale nic nebrání tomu, aby byl koncept použit ve vícevložkovém prostředí s 1 vlákno: 1 reaktor, čímž se využijí všechny jádra CPU.
Implementace
Veřejné rozhraní umístíme do souboru , a implementaci — do . reactor.h bude obsahovat následující deklarace:
Zobrazit deklarace v reactor.h
typedef struct reactor Reactor;
/*
* Ukazatel na funkci, která bude vyvolána I/O reaktorem při příjmu
* události od systémového selektoru.
*/
typedef void (*Callback)(void *arg, int fd, uint32_t events);
/*
* Vrací `NULL` v případě chyby, ne-`NULL` ukazatel na `Reactor` v
* opačném případě.
*/
Reactor *reactor_new(void);
/*
* Uvolňuje systémový selektor, všechny registrované sokety v daném okamžiku
* a samotný I/O reaktor.
*
* Následující funkce vrací -1 v případě chyby, 0 v případě úspěchu.
*/
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);
/*
* Spouští cyklus událostí s časovým limitem `timeout`.
*
* Tato funkce předá kontrolu volajícímu kódu, pokud uplynulý čas vypršel
* nebo/či při absenci registrovaných socketů.
*/
int reactor_run(const Reactor *reactor, time_t timeout);Struktura I/O reaktoru se skládá z selektoru a , která každý socket mapuje na CallbackData (struktura obsluhující událost a argument pro uživatele).
Zobrazit Reactor a CallbackData
struct reactor {
int epoll_fd;
GHashTable *table; // (int, CallbackData)
};
typedef struct {
Callback callback;
void *arg;
} CallbackData;Všimněte si, že jsme využili možnost manipulace s pomocí ukazatele. V reactor.h vyhlašujeme strukturu reactor, a v reactor.c a definujeme ji, čímž zabraňujeme uživateli explicitně měnit její pole. To je jedno z vzorů , které se elegantně vejde do semantiky C.
Funkce reactor_register, reactor_deregister a reactor_reregister aktualizují seznam zajímajících se socketů a odpovídajících obsluhovačů událostí v systému selektoru a v hash tabulce.
Zobrazit registrační funkce
#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;
}Po tom, co I/O reaktor zachytí událost s popisovačem fd, vyvolá odpovídající obsluhovač události, do kterého předává fd, vygenerovaných událostí a uživatelský ukazatel na void.
Zobrazit funkci 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) {
// Chyba
case -1:
perror("epoll_wait");
result = -1;
goto cleanup;
// Čas vypršel
case 0:
result = 0;
goto cleanup;
// Úspěšná operace
default:
// Zavolat zpracovatele událostí
for (int i = 0; i table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}Souhrnně, řetězec volání funkcí v uživatelském kódu bude mít následující podobu:

Jednovláknový server
Abychom otestovali I/O reaktor při vysokém zatížení, napíšeme jednoduchý HTTP webový server, který na jakýkoli dotaz odpoví obrázkem.
Krátká příručka k protokolu HTTP
— je to protokol , především používaný pro komunikaci serveru s prohlížečem.
HTTP lze snadno používat nad protokolu , posílající a přijímající zprávy ve formátu stanoveném .
Formát požadavku
CRLF
CRLF
CRLF
CRLF CRLF
CRLF— je to sekvence dvou znaků:jsou rovny nule.an, která odděluje první řádek požadavku, hlavičky a data.<КОМАНДА>— jedna zCONNECT,DELETE,GET,HEAD,OPTIONS,PATCH,POST,PUT,TRACE. Prohlížeč našemu serveru pošle příkazGET, což znamená „Pošli mi obsah souboru“.<URI>— . Například, pokud URI =/index.html, pak klient žádá o hlavní stránku webu.<ВЕРСИЯ HTTP>— verze protokolu HTTP ve formátuHTTP/X.Y. Nejčastěji používaná verze v současnosti jeHTTP/1.1.<ЗАГОЛОВОК N>— to je pár klíč-hodnota ve formátu:, posílané serveru k dalšímu zpracování.— data, která server potřebuje k provedení operace. Často je to jen nebo jakýkoli jiný formát.
Formát odpovědi
CRLF
CRLF
CRLF
CRLF CRLF
<КОД СТАТУСА>— je číslo, které představuje výsledek operace. Náš server bude vždy vracet stav 200 (úspěšná operace).<ОПИСАНИЕ СТАТУСА>— řetězcová reprezentace kódu stavu. Pro kód stavu 200 — to jeOK.<ЗАГОЛОВОК N>— hlavička ve stejném formátu jako v žádosti. Budeme vracet hlavičkyContent-Length(velikost souboru) aContent-Type: text/html(typ vracených dat).— uživatelsky požadovaná data. V našem případě je to cesta k obrázku v .
Soubor (jednoduchý server) zahrnuje soubor , který obsahuje následující prototypy funkcí:
Zobrazit prototypy funkcí v 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);Také je zde popsán funkční makro SAFE_CALL() a je definována funkce fail(). Makro porovnává hodnotu výrazu s chybou, a pokud je podmínka splněna, zavolá funkci fail():
#define SAFE_CALL(call, error)
do {
if ((call) == error) {
fail("%s", #call);
}
} while (false)Funkce fail() vytiskne předané argumenty do terminálu (jako ) a ukončuje program s kódem 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);
}Funkce new_server() vrací popisovač souboru "serverového" soketu, vytvořeného systémovými voláními , a a schopného přijímat příchozí spojení v neblokujícím režimu.
Zobrazit funkci 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;
}- Všimněte si, že soket je zpočátku vytvořen v neblokujícím režimu pomocí příznaku
SOCK_NONBLOCK, aby systémové volání v funkcion_accept()(číst dál) nezastavilo provádění vlákna.accept()reuse_port - Pokud
rovná se, pak tato funkce nakonfiguruje soket s možnostítrueSO_REUSEPORT setsockopt() Zpracovatel událostí
je volán poté, co operační systém vygeneruje událost on_accept() EPOLLIN , což v tomto případě znamená, že nové spojení může být přijato.přijímá nové spojení, přepíná ho do neblokujícího režimu a registruje s obsluhou události on_accept() on_recv() v I/O reaktoru. Zobrazit funkci on_accept()
Zobrazit funkci 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);
}je volán poté, co operační systém vygeneruje událost v I/O reaktoru. EPOLLIN , což v tomto případě znamená, že nové spojení může být přijato., v tomto případě to znamená, že spojení, které bylo registrováno on_accept(), je připraveno přijmout data.
v I/O reaktoru. čte data ze spojení, dokud není plně přijata HTTP žádost, poté registruje zpracovatele on_send() pro odeslání HTTP odpovědi. Pokud klient přeruší spojení, soket bude deregistrován a uzavřen pomocí .
Zobrazit funkci on_recv()
static void on_recv(void *arg, int fd, uint32_t events) {
RequestBuffer *buffer = arg;
// Přijímáme vstupní data, dokud recv nevrátí 0 nebo chybu
ssize_t nread;
while ((nread = recv(fd, buffer->data + buffer->size,
REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
buffer->size += nread;
// Klient přerušil spojení
if (nread == 0) {
SAFE_CALL(reactor_deregister(reactor, fd), -1);
SAFE_CALL(close(fd), -1);
request_buffer_destroy(buffer);
return;
}
// read vrátil chybu, která není chybou, kvůli které by se volání zablokovalo
// vlákno
if (errno != EAGAIN && errno != EWOULDBLOCK) {
request_buffer_destroy(buffer);
fail("read");
}
// Byla přijata úplná HTTP žádost od klienta. Nyní registrujeme zpracovatele
// událostí pro odesílání dat
if (request_buffer_is_complete(buffer)) {
request_buffer_clear(buffer);
SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
-1);
}
}je volán poté, co operační systém vygeneruje událost on_send() EPOLLIN EPOLLOUT, což znamená, že spojení, které bylo registrováno v I/O reaktoru., je připraveno odeslat data. Tato funkce odesílá HTTP odpověď obsahující HTML s obrázkem klientovi a poté znovu změní zpracovatele událostí na v I/O reaktoru..
Zobrazit funkci 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);
}A nakonec, v souboru http_server.c, ve funkci main() vytváříme I/O reaktor pomocí reactor_new(), vytváříme serverový soket a registrujeme ho, spustíme reaktor pomocí reactor_run() přesně na jednu minutu, poté uvolníme prostředky a opustíme program.
Zobrazit 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);
}Zkontrolujeme, že všechno funguje tak, jak má. Kompilujeme (chmod a+x compile.sh && ./compile.sh v kořenovém adresáři projektu) a spustíme náš vlastní server, otevřeme v prohlížeči a pozorujeme, co jsme očekávali:

Měření výkonu
Zobrazit specifikace mého stroje
$ 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
.MMMMMMMMMMMMMMMMMMMZměříme výkon jednovláknového serveru. Otevřeme dvě terminály: v jednom spustíme ./http_server, v druhém — . Po minutě se ve druhém terminálu zobrazí následující 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.19MBNáš jednovláknový server zvládl zpracovat přes 11 milionů požadavků za minutu vycházejících z 100 připojení. Dobré číslo, ale lze ho zlepšit?
Vícevláknový server
Jak bylo zmíněno výše, I/O reaktor lze vytvářet v samostatných vláknech, čímž se využijí všechny jádra CPU. Aplikujeme tento přístup v praxi:
Zobrazit 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);
}
}Nyní každé vlákno reaktor:
static Reactor *reactor;
#pragma omp threadprivate(reactor)Všimněte si, že argumentem funkce new_server() je true. To znamená, že serverovému socketu přiřazujeme možnost , abychom jej mohli využít v mnohovláknovém prostředí. Podrobnosti si můžete přečíst .
Druhá část
Nyní změříme výkon vícevláknového serveru:
$ 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.14MBPočet zpracovaných požadavků za 1 minutu vzrostl přibližně 3.28krát! Ale k zaokrouhlenému číslu chybělo pouze ~dva miliony, pokusíme se to opravit.
Nejprve se podíváme na statistiku, kterou jsme vygenerovali :
$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded
Statistika výkonu pro './http_server_multithreaded':
242446,314933 task-clock (ms) # 4,000 CPU využito
1 813 074 přepnutí kontextu # 0,007 M/sec
4 689 migrace CPU # 0,019 K/sec
254 stránkovací chyby # 0,001 K/sec
895 324 830 170 cykly # 3,693 GHz
621 378 066 808 instrukce # 0,69 insn na cyklus
119 926 709 370 větve # 494,653 M/sec
3 227 095 669 chyby větvení # 2,69% všech větví
808 664 chyby v cache
60,604330670 sekund uplynulo, kompilace s -march=native, , zvýšení počtu zásahů do , zvýšení MAX_EVENTS a využívání EPOLLET nepřineslo výrazný nárůst výkonu. Ale co se stane, pokud zvýšíme počet současných spojení?
Statistika při 352 současných spojení:
$ 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"
Běh testu po dobu 1m @ http://127.0.0.1:18470
8 vláken a 352 spojení
Statistiky vlákna Avg Stdev Max +/− Stdev
Latence 2.12ms 3.79ms 68.23ms 87.49%
Req/sec 83.78k 12.69k 169.81k 83.59%
40006142 požadavků za 1.00m, 5.48GB přečteno
Požadavky/sec: 665789.26
Přenos/sec: 93.34MBŽádaný výsledek byl dosažen, spolu s ním zajímavý graf ukazující závislost počtu zpracovaných požadavků za 1 minutu na počtu spojení:

Vidíme, že po několika stovkách spojení počet zpracovaných požadavků u obou serverů výrazně klesá (u vícerozměrné varianty je to výraznější). Je to spojeno s realizací TCP/IP protokolu v Linuxu? Své názory ohledně takového chování grafu a optimalizací vícerozměrných a jednorozměrných variant neváhejte psát do komentářů.
Jak V komentářích, tento test výkonu neukazuje chování I/O reactoru při skutečných zátěžích, protože téměř vždy server interaguje s DB, zapisuje logy, používá kryptografii s atd., což způsobuje, že zátěž se stává heterogenní (dynamickou). Testy spolu se třetími komponenty budou provedeny v článku o I/O reactoru.
Nevýhody I/O reactoru
Je třeba si uvědomit, že I/O reactor není bez nedostatků, a to konkrétně:
- Používání I/O reactoru v vícerozměrné prostředí je poněkud složitější, protože je nutné ručně řídit vlákna.
- Praxe ukazuje, že v většině případů je zatížení nehomogenní, což může vést k tomu, že jedna vlna bude zatěžovat, zatímco druhá bude zaměstnána prací.
- Pokud jeden zpracovatel události zablokuje vlákno, pak se také zablokuje samotný systémový selektor, což může vést k těžko odhalitelným chybám.
Tyto problémy řeší , který obvykle má plánovač, který rovnoměrně rozděluje zátěž v poolu vláken, a také má pohodlnější API. O něm bude řeč později, v mém jiném článku.
Závěr
Na tomto našem putování z teorie přímo k profilingovému výstupu jsme skončili.
Není dobré na tom zastavit, existuje mnoho dalších zajímavých přístupů k psaní síťového softwaru s různou úrovní pohodlí a rychlosti. Zajímavé odkazy, jak se domnívám, jsou uvedeny níže.
Na shledanou!
Zajímavé projekty
- C
Co ještě číst?
Zdroj: habr.com
