Функционален I/O реактор на гол C

Функционален I/O реактор на гол C

Въведение

I/O реактор (единичен поток цикъл на събития) — това е шаблон за писане на приложения с висока натовареност, използван в много популярни решения:

В тази статия ще разгледаме основите на I/O реактора и принципа на неговата работа, ще напишем реализация с по-малко от 200 реда код и ще накараме прост HTTP сървър да обработва над 40 милиона заявки/мин.

Предговор

  • Статията е написана с цел да помогне да се разбере функционирането на I/O реактора, а следователно и да се осъзнаят рисковете при неговото използване.
  • За усвояването на статията е необходимо да имате основи езика C и опит в разработването на мрежови приложения.
  • Целият код е написан на езика C стриктно според (внимание: дълъг PDF) стандарта C11 за Linux и е достъпен на GitHub.

Защо е необходимо?

С нарастващата популярност на интернет, уеб сървърите започнаха да се нуждаят от обработка на голям брой съединения едновременно, в резултат на което бяха изпробвани два подхода: блокиращо I/O с голям брой потоци на ОС и неблокиращо I/O в комбинация със система за уведомяване за събития, известна още като «системен селектор» (epoll/kqueue/IOCP/etc).

Първият подход предвиждаше създаване на нов поток на ОС за всяко входящо съединение. Неговият недостатък е лошата мащабируемост: операционната система трябва да извърши множество превключвания на контекста и системни викания. Те са скъпи операции и могат да доведат до недостиг на свободна RAM при значителен брой съединения.

Модифицираната версия определя фиксиран брой потоци (пул от потоци), като по този начин не позволява на системата да прекрати изпълнението аварийно, но в същото време внася нова проблематика: ако в момента пулът от потоци блокира продължителни операции на четене, то другите сокети, които вече са в състояние да приемат данни, не могат да го направят.

Вторият подход използва система за уведомяване за събития (системен селектор), предоставена от ОС. В тази статия ще разгледаме най-често срещаната форма на системен селектор, основана на уведомления (събития, уведомления) за готовност за I/O операции, вместо на уведомления за тяхното завършване. Опростен пример за неговото използване може да бъде представен в следната блок-схема:

Функционален I/O реактор на гол C

Разликата между тези подходи е следната:

  • Блокиращите I/O операции спират потребителския поток докато AWS не направи същото и не изгради успешен бизнес около него, и когато клиентите буквално поискат същото. Все пак трябва да се положат известни усилия, за да накарате Google да поддържа нещо., докато ОС не дефрагментира постъпващите IP пакети в поток от байтове (TCP, получаване на данни) или не остава достатъчно място във вътрешните буфери за запис за последваща изпращане чрез NIC (изпращане на данни).
  • Системният селектор след известно време уведомява програмата, че ОС вече е дефрагментирала IP пакетите (TCP, получаване на данни) или достатъчно място във вътрешните буфери за запис вече е налично (изпращане на данни).

В заключение, резервацията на потока на ОС за всяко I/O е напразно изразходване на изчислителна мощ, тъй като на практика потоците не са заети с полезна работа (тук произлиза терминът „програмно прекъсване“). Системният селектор решава този проблем, позволявайки на потребителската програма да използва ресурсите на CPU много по-икономично.

Моделът на I/O реактора

I/O реакторът действа като слой между системния селектор и потребителския код. Принципът на работа е описан в следната блок-схема:

Функционален I/O реактор на гол C

  • Да напомня, че събитието е уведомление, че определен сокет е в състояние да изпълни неблокираща I/O операция.
  • Обработчикът на събития е функция, извиквана от I/O реактора при получаване на събитие, която след това извършва неблокираща I/O операция.

Важно е да се отбележи, че I/O реакторът по дефиниция е еднопотоков, но нищо не пречи концепцията да се използва в многопоточна среда в съотношение 1 поток: 1 реактор, като по този начин се използват всички ядра на CPU.

Реализация

Публичният интерфейс ще бъде поставен в файл reactor.h, а реализацията — в reactor.c. reactor.h ще се състои от следните декларации:

Показване на декларации в reactor.h

typedef struct reactor Reactor;

/*
 * Показател на функция, която ще бъде извикана от I/O реактора при получаване
 * на събитие от системния селектор.
 */
typedef void (*Callback)(void *arg, int fd, uint32_t events);

/*
 * Връща `NULL` в случай на грешка, не-`NULL` указател на `Reactor` в
 * противен случай.
 */
Reactor *reactor_new(void);

/*
 * Освобождава системния селектор, всички регистрирани сокети в момента
 * и самия I/O реактор.
 *
 * Следните функции връщат -1 в случай на грешка, 0 в случай на успех.
 */
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);

/*
 * Стартира цикъл от събития с таймаут `timeout`.
 *
 * Тази функция ще предаде управлението на извикващия код, ако отреденото време е изтекло
 * или/и при липса на регистрирани сокети.
 */
int reactor_run(const Reactor *reactor, time_t timeout);

Структурата на I/O реактора се състои от файлов дескриптор селектор epoll и хеш-таблица GHashTable, която съпоставя всеки сокет с CallbackData (структура от обработчик на събитие и аргумент за потребителя към него).

Покажи Reactor и CallbackData

struct reactor {
    int epoll_fd;
    GHashTable *table; // (int, CallbackData)
};

typedef struct {
    Callback callback;
    void *arg;
} CallbackData;

Обърнете внимание, че сме използвали възможността за работа с непълен тип по указател. В reactor.h обявяваме структурата reactor, а в reactor.c я дефинираме, така че да не позволим на потребителя явно да променя полетата ѝ. Това е един от моделите скриване на данни, лаконично вписващ се в семантиката на Си.

Функции reactor_register, reactor_deregister и reactor_reregister обновяват списъка на интересуващите сокети и съответстващите обработчици на събития в системния селектор и в хеш-таблицата.

Покажи функции за регистрация

#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;
}

След като I/O реакторът е уловил събитие с дескриптор fd, той извиква съответния обработчик на събития, в който предава fd, битова маска на генерираните събития и потребителски указател на празен.

Покажи функцията 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) {
        // Грешка
        case -1:
            perror("epoll_wait");
            result = -1;
            goto cleanup;
        // Времето изтече
        case 0:
            result = 0;
            goto cleanup;
        // Успешна операция
        default:
            // Извикване на обработчиците на събития
            for (int i = 0; i table, &fd);
                callback->callback(callback->arg, fd, events[i].events);
            }
        }
    }

cleanup:
    free(events);
    return result;
}

В обобщение, веригата от извиквания на функции в потребителския код ще изглежда по следния начин:

Функционален I/O реактор на гол C

Однопоточен сървър

За да тестваме I/O реактора под високо натоварване, ще напишем прост HTTP уеб сървър, който отговаря с изображение на всяка заявка.

Кратка информация за протоколa HTTP

HTTP — това е протокол на приложно ниво, който се използва основно за взаимодействие между сървъра и браузъра.

HTTP може лесно да се използва над транспортния протокол TCP, изпращайки и получавайки съобщения във формата, определен от спецификацията.

Формат на заявка

CRLF
CRLF
CRLF
CRLF CRLF

  • CRLF — това е последователност от два символа: r и n, разделяща първия ред на заявката, заглавията и данните.
  • <КОМАНДА> — една от CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE. Браузерът ще изпрати на нашия сървър команда, GET, която означава "Изпрати ми съдържанието на файла".
  • <URI>унифициран идентификатор на ресурса. Например, ако URI = /index.html, клиентът иска началната страница на сайта.
  • <ВЕРСИЯ HTTP> — версия на протокола HTTP в формата HTTP/X.Y. Най-често използваната версия в днешно време е HTTP/1.1.
  • <ЗАГОЛОВОК N> — това е двойка ключ-стойност в формата :, изпращана на сървъра за по-нататъшен анализ.
  • <ДАННЫЕ> — данни, необходими на сървъра за изпълнение на операцията. Често това е просто JSON или друг формат.

Формат на отговор

CRLF
CRLF
CRLF
CRLF CRLF

  • <КОД СТАТУСА> — това число представлява резултата от операцията. Нашият сървър винаги ще връща статус 200 (успешна операция).
  • <ОПИСАНИЕ СТАТУСА> — низово представяне на статус кода. За статус кода 200 — това е OK.
  • <ЗАГОЛОВОК N> — заглавие в същия формат като в заявката. Ще връщаме заглавия Content-Length (размер на файла) и Content-Type: text/html (тип връщани данни).
  • <ДАННЫЕ> — данните, изисквани от потребителя. В нашия случай това е пътят към изображението в HTML.

Файл http_server.c (еднопоточен сървър) включва файла common.h, който съдържа следните прототипи на функции:

Покажи прототипи на функции в 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);

Също така е описан функционален макрос SAFE_CALL() и е определена функция fail(). Макросът сравнява стойността на израза с грешка, и ако условието е изпълнено, извиква функцията fail():

#define SAFE_CALL(call, error)                                                 
    do {                                                                       
        if ((call) == error) {                                                   
            fail("%s", #call);                                                 
        }                                                                      
    } while (false)

Функция fail() извежда предадените аргументи в терминала (като printf()) и завършва изпълнението на програмата с код 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);
}

Функция new_server() върща файлов дескриптор на "сървърния" сокет, създаден от системните извиквания socket(), bind() и listen() и способен да приема входящи връзки в неблокиращ режим.

Покажи функция 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;
}

се извиква след като ОС генерира събитие on_accept() EPOLLIN , в този случай означаващо, че нова връзка може да бъде приета.приема нова връзка, премества я в неблокиращ режим и я регистрира с обработчика на събития on_accept() on_recv() в I/O реактора. Покажи функция on_accept()

Показване на функцията 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);
}

се извиква след като ОС генерира събитие в I/O реактора. EPOLLIN , в този случай означаващо, че нова връзка може да бъде приета., което означава, че регистрираната връзка on_accept(), е готова да приема данни.

в I/O реактора. чете данни от връзката, докато HTTP заявката е напълно получена, след което регистрира обработчик on_send() за изпращане на HTTP отговор. Ако клиентът прекъсне връзката, сокетът се дерегистрира и затваря чрез close().

Покажи функцията on_recv()

static void on_recv(void *arg, int fd, uint32_t events) {
    RequestBuffer *buffer = arg;

    // Прийом на входящите данни, докато recv не върне 0 или грешка
    ssize_t nread;
    while ((nread = recv(fd, buffer->data + buffer->size,
                         REQUEST_BUFFER_CAPACITY - buffer->size, 0)) > 0)
        buffer->size += nread;

    // Клиентът прекъсна връзката
    if (nread == 0) {
        SAFE_CALL(reactor_deregister(reactor, fd), -1);
        SAFE_CALL(close(fd), -1);
        request_buffer_destroy(buffer);
        return;
    }

    // read върна грешка, различна от грешка, при която извикването блокира
    // потока
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
        request_buffer_destroy(buffer);
        fail("read");
    }

    // Получена е пълна HTTP заявка от клиента. Сега регистрираме обработчика
    // на събитията за изпращане на данни
    if (request_buffer_is_complete(buffer)) {
        request_buffer_clear(buffer);
        SAFE_CALL(reactor_reregister(reactor, fd, EPOLLOUT, on_send, buffer),
                  -1);
    }
}

се извиква след като ОС генерира събитие on_send() EPOLLIN EPOLLOUT, което означава, че регистрираната връзка в I/O реактора., е готова да изпраща данни. Тази функция изпраща HTTP отговор, съдържащ HTML с изображение, на клиента, а след това отново променя обработчика на събитията на в I/O реактора..

Покажи функцията on_send()

статичен 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);
}

И накрая, в файла http_server.c, във функцията main() създаваме I/O реактор с помощта на reactor_new(), създаваме сървърен сокет и го регистрираме, стартираме реактора с помощта на reactor_run() точно за една минута, след което освобождаваме ресурсите и излизаме от програмата.

Покажи 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);
}

Нека проверим, че всичко работи както трябва. Компилираме (chmod a+x compile.sh && ./compile.sh в корена на проекта) и стартираме самостоятелния сървър, отваряме http://127.0.0.1:18470 в браузъра и наблюдаваме това, което очаквахме:

Функционален I/O реактор на гол C

Тест за производителност

Покажи характеристиките на моята машина

$ 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
             .MMMMMMMMMMMMMMMMMMM

Измерваме производителността на еднопоточен сървър. Отваряме два терминала: в единия стартираме ./http_server, а в другия — wrk. След минута във втория терминал ще се появи следната статистика:

$ 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.19MB

Нашият еднопоточен сървър успя да обработи над 11 милиона заявки в минута, идващи от 100 връзки. Добр резултат, но можем ли да го подобрим?

Многопоточен сървър

Както беше споменато по-горе, I/O реакторът може да се създаде в отделни потоци, като по този начин се оползотворяват всичките ядра на ЦПУ. Нека приложим този подход на практика:

Показване на 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);
    }
}

Сега всеки поток разполага с собствен реактор:

static Reactor *reactor;
#pragma omp threadprivate(reactor)

Обърнете внимание, че аргументът на функцията new_server() се застъпва true. Това означава, че задаваме опцията на сървърния сокет чрез, за да го използваме в многопоточна среда. Можете да прочетете повече тук.

Второ опитване

Сега ще измерим производителността на многопоточния сървър:

$ 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

Броят на обработените заявки за 1 минута се увеличи с ~3.28 пъти! Но до кръгло число не достигнаха едва ~два милиона, нека опитаме да го поправим.

Първо, нека да разгледаме статистиката, генерирана perf:

$ sudo perf stat -B -e task-clock,context-switches,cpu-migrations,page-faults,cycles,instructions,branches,branch-misses,cache-misses ./http_server_multithreaded

 Performance counter stats for './http_server_multithreaded':

     242446,314933      task-clock (msec)         #    4,000 CPUs utilized          
         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% of all branches        
           808 664      cache-misses                                                

      60,604330670 seconds time elapsed

Използване на CPU аффинитет, компилиране с -march=native, PGO, увеличаване на броя на попаденията в кеша, увеличаване на MAX_EVENTS и използване на EPOLLET не доведе до значително повишение на производителността. Но какво ще стане, ако увеличим броя на едновременните връзки?

Статистика при 352 едновременни връзки:

$ 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"
Running 1m test @ http://127.0.0.1:18470
  8 threads and 352 connections
  Thread Stats   Avg      Stdev     Max   +/-- Stdev
    Latency     2.12ms    3.79ms  68.23ms   87.49%
    Req/Sec    83.78k    12.69k  169.81k    83.59%
  40006142 requests in 1.00m, 5.48GB read
Requests/sec: 665789.26
Transfer/sec:     93.34MB

Желаният резултат е постигнат, а заедно с него и интересна графика, демонстрираща зависимостта на броя на обработените заявки за 1 минута от броя на връзките:

Функционален I/O реактор на гол C

Виждаме, че след няколко стотин връзки, броят на обработените заявки рязко намалява и при двата сървъра (при многопоточния вариант е по-забележимо). Свързано ли е това с реализирания TCP/IP стек на Linux? Споделете вашите предположения относно такова поведение на графиката и оптимизации на многопоточните и еднопоточни варианти в коментарите.

Как подчертали в коментарите, този тест за производителност не показва поведението на I/O реактора при реални натоварвания, тъй като почти винаги сървърът взаимодейства с БД, извежда логове, използва криптография с TLS и т.н., в резултат на което натоварването става нехомогенно (динамично). Тестовете заедно с външни компоненти ще бъдат проведени в статия за I/O проактора.

Недостатъци на I/O реактора

Трябва да разберем, че I/O реакторът не е без недостатъци, а именно:

  • Използването на I/O реактора в многопоточна среда е малко по-сложно, тъй като ще се наложи ръчно управление на потоковете.
  • Практиката показва, че в повечето случаи натоварването е нееднородно, което може да доведе до ситуация, в която един поток ще обработва данни, докато друг ще бъде зает с работа.
  • Ако един обработчик на събития блокира потока, то също ще блокира и самия системен селектор, което може да доведе до трудно откриваеми бъгове.

Тези проблеми решава I/O проактор, който обикновено има планировчик, който равномерно разпределя натоварването в пул от потоци, и също така предлага по-удобен API. За него ще говорим по-късно в моята друга статия.

Заключение

С това нашето пътуване от теория направо към изпускането на профайлера приключи.

Не бива да спираме дотук, тъй като съществуват множество други не по-малко интересни подходи за създаване на мрежов софтуер с различно ниво на удобство и скорост. Интересни, според мен, връзки са посочени по-долу.

До нови срещи!

Интересни проекти

Какво още да прочетете?

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster