I/O αντιδραστήρας (μονό σπείρωμα βρόχος συμβάντος) είναι ένα μοτίβο για τη σύνταξη λογισμικού υψηλού φορτίου, που χρησιμοποιείται σε πολλές δημοφιλείς λύσεις:
Σε αυτό το άρθρο, θα εξετάσουμε τις λεπτομέρειες ενός αντιδραστήρα I/O και πώς λειτουργεί, θα γράψουμε μια υλοποίηση σε λιγότερες από 200 γραμμές κώδικα και θα κάνουμε μια απλή διαδικασία διακομιστή HTTP πάνω από 40 εκατομμύρια αιτήματα/λεπτό.
πρόλογος
Το άρθρο γράφτηκε για να βοηθήσει στην κατανόηση της λειτουργίας του αντιδραστήρα εισόδου/εξόδου και, επομένως, στην κατανόηση των κινδύνων κατά τη χρήση του.
Απαιτείται γνώση των βασικών για την κατανόηση του άρθρου. Γλώσσα Γ και κάποια εμπειρία στην ανάπτυξη εφαρμογών δικτύου.
Όλος ο κώδικας είναι γραμμένος στη γλώσσα C αυστηρά σύμφωνα με (Προσοχή: μεγάλο PDF) στο πρότυπο C11 για Linux και διαθέσιμο στο GitHub.
Γιατί το κάνει;
Με την αυξανόμενη δημοτικότητα του Διαδικτύου, οι διακομιστές ιστού άρχισαν να χρειάζεται να χειρίζονται μεγάλο αριθμό συνδέσεων ταυτόχρονα, και ως εκ τούτου δοκιμάστηκαν δύο προσεγγίσεις: αποκλεισμός I/O σε μεγάλο αριθμό νημάτων του λειτουργικού συστήματος και μη αποκλεισμός I/O σε συνδυασμό με ένα σύστημα ειδοποίησης συμβάντων, που ονομάζεται επίσης "επιλογέας συστήματος" (επολ/ουρά/IOCP/και τα λοιπά).
Η πρώτη προσέγγιση περιλάμβανε τη δημιουργία ενός νέου νήματος λειτουργικού συστήματος για κάθε εισερχόμενη σύνδεση. Το μειονέκτημά του είναι η κακή επεκτασιμότητα: το λειτουργικό σύστημα θα πρέπει να εφαρμόσει πολλά μεταβάσεις περιβάλλοντος и κλήσεις συστήματος. Είναι ακριβές λειτουργίες και μπορεί να οδηγήσουν σε έλλειψη ελεύθερης μνήμης RAM με εντυπωσιακό αριθμό συνδέσεων.
Η τροποποιημένη έκδοση επισημαίνει σταθερός αριθμός νημάτων (ομάδα νημάτων), εμποδίζοντας έτσι το σύστημα να ματαιώσει την εκτέλεση, αλλά ταυτόχρονα εισάγει ένα νέο πρόβλημα: εάν μια ομάδα νημάτων είναι επί του παρόντος αποκλεισμένη από λειτουργίες μακράς ανάγνωσης, τότε άλλες υποδοχές που είναι ήδη σε θέση να λάβουν δεδομένα δεν θα μπορούν να να το κάνεις.
Η δεύτερη προσέγγιση χρησιμοποιεί σύστημα ειδοποίησης συμβάντων (επιλογέας συστήματος) που παρέχεται από το Λ.Σ. Αυτό το άρθρο εξετάζει τον πιο συνηθισμένο τύπο επιλογέα συστήματος, με βάση ειδοποιήσεις (συμβάντα, ειδοποιήσεις) σχετικά με την ετοιμότητα για λειτουργίες I/O, αντί για ειδοποιήσεις για την ολοκλήρωσή τους. Ένα απλοποιημένο παράδειγμα χρήσης του μπορεί να αναπαρασταθεί από το ακόλουθο μπλοκ διάγραμμα:
Η διαφορά μεταξύ αυτών των προσεγγίσεων είναι η εξής:
Αποκλεισμός λειτουργιών I/O αναστέλλω ροή χρήστη μέχριμέχρι το λειτουργικό σύστημα να είναι σωστά ανασυγκροτήσεις εισερχόμενος Πακέτα IP σε ροή byte (TCP, λήψη δεδομένων) ή δεν θα υπάρχει αρκετός διαθέσιμος χώρος στα εσωτερικά buffer εγγραφής για μετέπειτα αποστολή μέσω NIC (αποστολή στοιχείων).
Επιλογέας συστήματος στο περασμα του χρονου ειδοποιεί το πρόγραμμα ότι το Λ.Σ ήδη ανασυγκροτημένα πακέτα IP (TCP, λήψη δεδομένων) ή αρκετός χώρος σε εσωτερικά buffer εγγραφής ήδη διαθέσιμα (αποστολή δεδομένων).
Συνοψίζοντας, η κράτηση ενός νήματος λειτουργικού συστήματος για κάθε I/O είναι σπατάλη υπολογιστικής ισχύος, επειδή στην πραγματικότητα, τα νήματα δεν κάνουν χρήσιμη δουλειά (εξ ου και ο όρος "διακοπή λογισμικού"). Ο επιλογέας συστήματος λύνει αυτό το πρόβλημα, επιτρέποντας στο πρόγραμμα χρήστη να χρησιμοποιεί τους πόρους της CPU πολύ πιο οικονομικά.
Μοντέλο αντιδραστήρα I/O
Ο αντιδραστήρας I/O λειτουργεί ως στρώμα μεταξύ του επιλογέα συστήματος και του κωδικού χρήστη. Η αρχή της λειτουργίας του περιγράφεται από το ακόλουθο μπλοκ διάγραμμα:
Επιτρέψτε μου να σας υπενθυμίσω ότι ένα συμβάν είναι μια ειδοποίηση ότι μια συγκεκριμένη υποδοχή μπορεί να εκτελέσει μια λειτουργία I/O χωρίς αποκλεισμό.
Ένας χειριστής συμβάντων είναι μια συνάρτηση που καλείται από τον αντιδραστήρα I/O όταν λαμβάνεται ένα συμβάν, το οποίο στη συνέχεια εκτελεί μια λειτουργία I/O χωρίς αποκλεισμό.
Είναι σημαντικό να σημειωθεί ότι ο αντιδραστήρας I/O είναι εξ ορισμού μονού νήματος, αλλά τίποτα δεν εμποδίζει τη χρήση της ιδέας σε περιβάλλον πολλαπλών νημάτων σε αναλογία 1 νήμα: 1 αντιδραστήρα, ανακυκλώνοντας έτσι όλους τους πυρήνες της CPU.
Реализация
Θα τοποθετήσουμε τη δημόσια διεπαφή σε ένα αρχείο reactor.h, και υλοποίηση - in reactor.c. reactor.h θα αποτελείται από τις ακόλουθες ανακοινώσεις:
Εμφάνιση δηλώσεων στον αντιδραστήρα.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 αποτελείται από περιγραφέας αρχείου εκλέκτορας επολ и πίνακες κατακερματισμούGHashTable, το οποίο αντιστοιχίζει κάθε υποδοχή σε CallbackData (δομή ενός χειριστή συμβάντων και ένα όρισμα χρήστη για αυτό).
Λάβετε υπόψη ότι έχουμε ενεργοποιήσει τη δυνατότητα χειρισμού ημιτελής τύπος σύμφωνα με τον δείκτη. ΣΕ reactor.h δηλώνουμε τη δομή reactor, ενώ στο reactor.c το ορίζουμε, εμποδίζοντας έτσι τον χρήστη να αλλάξει ρητά τα πεδία του. Αυτό είναι ένα από τα μοτίβα απόκρυψη δεδομένων, το οποίο εντάσσεται συνοπτικά στη σημασιολογία C.
Λειτουργίες reactor_register, reactor_deregister и reactor_reregister ενημερώστε τη λίστα των υποδοχών που σας ενδιαφέρουν και των αντίστοιχων χειριστών συμβάντων στον επιλογέα συστήματος και στον πίνακα κατακερματισμού.
Αφού ο αντιδραστήρας I/O αναχαιτίσει το συμβάν με τον περιγραφέα fd, καλεί τον αντίστοιχο χειριστή συμβάντων, στον οποίο μεταβιβάζει fd, μάσκα μπιτ συμβάντα που δημιουργούνται και έναν δείκτη χρήστη σε void.
Εμφάνιση συνάρτησης 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 < nfds; i++) {
int fd = events[i].data.fd;
CallbackData *callback =
g_hash_table_lookup(reactor->table, &fd);
callback->callback(callback->arg, fd, events[i].events);
}
}
}
cleanup:
free(events);
return result;
}
Συνοψίζοντας, η αλυσίδα των κλήσεων συναρτήσεων στον κωδικό χρήστη θα έχει την ακόλουθη μορφή:
Διακομιστής με ένα σπείρωμα
Προκειμένου να δοκιμάσουμε τον αντιδραστήρα I/O υπό υψηλό φορτίο, θα γράψουμε έναν απλό διακομιστή web HTTP που ανταποκρίνεται σε οποιοδήποτε αίτημα με μια εικόνα.
Μια γρήγορη αναφορά στο πρωτόκολλο HTTP
HTTP - αυτό είναι το πρωτόκολλο επίπεδο εφαρμογής, που χρησιμοποιείται κυρίως για την αλληλεπίδραση διακομιστή-προγράμματος περιήγησης.
Το HTTP μπορεί να χρησιμοποιηθεί εύκολα μεταφορά πρωτόκολλο TCP, αποστολή και λήψη μηνυμάτων σε καθορισμένη μορφή προσδιορισμός.
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 ή οποιαδήποτε άλλη μορφή.
<КОД СТАТУСА> είναι ένας αριθμός που αντιπροσωπεύει το αποτέλεσμα της επέμβασης. Ο διακομιστής μας θα επιστρέφει πάντα την κατάσταση 200 (επιτυχής λειτουργία).
<ОПИСАНИЕ СТАТУСА> — αναπαράσταση συμβολοσειράς του κωδικού κατάστασης. Για τον κωδικό κατάστασης 200 αυτό είναι OK.
<ЗАГОЛОВОК N> — κεφαλίδα της ίδιας μορφής όπως στο αίτημα. Θα επιστρέψουμε τους τίτλους Content-Length (μέγεθος αρχείου) και Content-Type: text/html (τύπος δεδομένων επιστροφής).
<ДАННЫЕ> — δεδομένα που ζητούνται από τον χρήστη. Στην περίπτωσή μας, αυτή είναι η διαδρομή προς την εικόνα μέσα HTML.
αρχείο http_server.c (διακομιστής με ένα νήμα) περιλαμβάνει αρχείο common.h, το οποίο περιέχει τα ακόλουθα πρωτότυπα συναρτήσεων:
Εμφάνιση πρωτοτύπων συναρτήσεων κοινά.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:
Λειτουργία new_server() επιστρέφει τον περιγραφέα αρχείου της υποδοχής "διακομιστής" που δημιουργήθηκε από κλήσεις συστήματος socket(), bind() и listen() και μπορεί να δέχεται εισερχόμενες συνδέσεις σε λειτουργία μη αποκλεισμού.
Σημειώστε ότι η πρίζα δημιουργείται αρχικά σε λειτουργία μη αποκλεισμού χρησιμοποιώντας τη σημαία SOCK_NONBLOCKώστε στη συνάρτηση on_accept() (διαβάστε περισσότερα) κλήση συστήματος accept() δεν σταμάτησε την εκτέλεση του νήματος.
αν reuse_port ισούται με true, τότε αυτή η λειτουργία θα διαμορφώσει την υποδοχή με την επιλογή SO_REUSEPORT μέσω setsockopt()για να χρησιμοποιήσετε την ίδια θύρα σε περιβάλλον πολλαπλών νημάτων (βλ. ενότητα «Διακομιστής πολλαπλών νημάτων»).
Χειριστής συμβάντων on_accept() καλείται αφού το λειτουργικό σύστημα δημιουργήσει ένα συμβάν EPOLLIN, σε αυτήν την περίπτωση σημαίνει ότι η νέα σύνδεση μπορεί να γίνει αποδεκτή. on_accept() αποδέχεται μια νέα σύνδεση, τη μεταβαίνει σε λειτουργία μη αποκλεισμού και εγγράφεται σε έναν χειριστή συμβάντων on_recv() σε αντιδραστήρα I/O.
Χειριστής συμβάντων on_recv() καλείται αφού το λειτουργικό σύστημα δημιουργήσει ένα συμβάν EPOLLIN, στην περίπτωση αυτή σημαίνει ότι η σύνδεση καταχωρήθηκε on_accept(), έτοιμο να λάβει δεδομένα.
on_recv() διαβάζει δεδομένα από τη σύνδεση μέχρι να ληφθεί πλήρως το αίτημα 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() καλείται αφού το λειτουργικό σύστημα δημιουργήσει ένα συμβάν EPOLLOUT, που σημαίνει ότι η σύνδεση καταχωρήθηκε on_recv(), έτοιμο για αποστολή δεδομένων. Αυτή η συνάρτηση στέλνει μια απάντηση HTTP που περιέχει HTML με μια εικόνα στον πελάτη και, στη συνέχεια, αλλάζει το πρόγραμμα χειρισμού συμβάντων σε on_recv().
Και τέλος, στο αρχείο http_server.c, σε λειτουργία main() δημιουργούμε έναν αντιδραστήρα I/O χρησιμοποιώντας reactor_new(), δημιουργήστε μια υποδοχή διακομιστή και καταχωρήστε την, ξεκινήστε τον αντιδραστήρα χρησιμοποιώντας reactor_run() για ακριβώς ένα λεπτό, και στη συνέχεια απελευθερώνουμε πόρους και βγαίνουμε από το πρόγραμμα.
Ας ελέγξουμε ότι όλα λειτουργούν όπως αναμένεται. Σύνταξη (chmod a+x compile.sh && ./compile.sh στη ρίζα του έργου) και εκκινήστε τον αυτο-γραμμένο διακομιστή, ανοίξτε http://127.0.0.1:18470 στο πρόγραμμα περιήγησης και δείτε τι περιμέναμε:
Ας μετρήσουμε την απόδοση ενός διακομιστή με ένα νήμα. Ας ανοίξουμε δύο τερματικά: στο ένα θα τρέξουμε ./http_server, σε μια διαφορετική - σκατά. Μετά από ένα λεπτό, τα ακόλουθα στατιστικά στοιχεία θα εμφανιστούν στο δεύτερο τερματικό:
Ο διακομιστής μας με ένα νήμα ήταν σε θέση να επεξεργαστεί πάνω από 11 εκατομμύρια αιτήματα ανά λεπτό που προέρχονται από 100 συνδέσεις. Δεν είναι κακό αποτέλεσμα, αλλά μπορεί να βελτιωθεί;
Διακομιστής πολλαπλών νημάτων
Όπως αναφέρθηκε παραπάνω, ο αντιδραστήρας I/O μπορεί να δημιουργηθεί σε ξεχωριστά νήματα, χρησιμοποιώντας έτσι όλους τους πυρήνες της CPU. Ας κάνουμε πράξη αυτήν την προσέγγιση:
Σημειώστε ότι το όρισμα συνάρτησης new_server() υποστηρίζει true. Αυτό σημαίνει ότι εκχωρούμε την επιλογή στην υποδοχή διακομιστή SO_REUSEPORTγια να το χρησιμοποιήσετε σε περιβάλλον πολλαπλών νημάτων. Μπορείτε να διαβάσετε περισσότερες λεπτομέρειες εδώ.
Δεύτερο τρέξιμο
Τώρα ας μετρήσουμε την απόδοση ενός διακομιστή πολλαπλών νημάτων:
Ο αριθμός των αιτημάτων που υποβλήθηκαν σε επεξεργασία σε 1 λεπτό αυξήθηκε κατά ~3.28 φορές! Αλλά μας έλειπαν μόνο ~ XNUMX εκατομμύρια από τον αριθμό του γύρου, οπότε ας προσπαθήσουμε να το διορθώσουμε.
Πρώτα ας δούμε τα στατιστικά που δημιουργούνται Perf:
Χρήση CPU Affinity, συλλογή με -march=native, PGO, αύξηση του αριθμού των επισκέψεων μετρητά, αυξάνουν MAX_EVENTS και χρήση EPOLLET δεν έδωσε σημαντική αύξηση στην απόδοση. Τι θα συμβεί όμως αν αυξήσετε τον αριθμό των ταυτόχρονων συνδέσεων;
Στατιστικά στοιχεία για 352 ταυτόχρονες συνδέσεις:
Λήφθηκε το επιθυμητό αποτέλεσμα και μαζί του ένα ενδιαφέρον γράφημα που δείχνει την εξάρτηση του αριθμού των επεξεργασμένων αιτημάτων σε 1 λεπτό από τον αριθμό των συνδέσεων:
Βλέπουμε ότι μετά από μερικές εκατοντάδες συνδέσεις, ο αριθμός των επεξεργασμένων αιτημάτων και για τους δύο διακομιστές μειώνεται απότομα (στην έκδοση πολλαπλών νημάτων αυτό είναι πιο αισθητό). Σχετίζεται αυτό με την υλοποίηση στοίβας TCP/IP του Linux; Μη διστάσετε να γράψετε τις υποθέσεις σας σχετικά με αυτήν τη συμπεριφορά του γραφήματος και τις βελτιστοποιήσεις για επιλογές πολλαπλών νημάτων και μονών νημάτων στα σχόλια.
πως διάσημος στα σχόλια, αυτή η δοκιμή απόδοσης δεν δείχνει τη συμπεριφορά του αντιδραστήρα I/O κάτω από πραγματικά φορτία, επειδή σχεδόν πάντα ο διακομιστής αλληλεπιδρά με τη βάση δεδομένων, εξάγει αρχεία καταγραφής, χρησιμοποιεί κρυπτογραφία με TLS κ.λπ., με αποτέλεσμα το φορτίο να γίνεται ανομοιόμορφο (δυναμικό). Οι δοκιμές μαζί με στοιχεία τρίτων θα πραγματοποιηθούν στο άρθρο σχετικά με τον προβολέα I/O.
Μειονεκτήματα του αντιδραστήρα I/O
Πρέπει να καταλάβετε ότι ο αντιδραστήρας I/O δεν είναι χωρίς μειονεκτήματα, δηλαδή:
Η χρήση ενός αντιδραστήρα I/O σε περιβάλλον πολλαπλών νημάτων είναι κάπως πιο δύσκολη, γιατί θα πρέπει να διαχειριστείτε χειροκίνητα τις ροές.
Η πρακτική δείχνει ότι στις περισσότερες περιπτώσεις το φορτίο είναι ανομοιόμορφο, γεγονός που μπορεί να οδηγήσει σε καταγραφή ενός νήματος ενώ ένα άλλο είναι απασχολημένο με εργασία.
Εάν ένας χειριστής συμβάντων μπλοκάρει ένα νήμα, τότε ο ίδιος ο επιλογέας συστήματος θα αποκλείσει επίσης, γεγονός που μπορεί να οδηγήσει σε δυσεύρετα σφάλματα.
Λύνει αυτά τα προβλήματα I/O proactor, το οποίο συχνά έχει έναν χρονοπρογραμματιστή που κατανέμει ομοιόμορφα το φορτίο σε μια δεξαμενή νημάτων και έχει επίσης ένα πιο βολικό API. Θα μιλήσουμε για αυτό αργότερα, σε άλλο άρθρο μου.
Συμπέρασμα
Εδώ τελείωσε το ταξίδι μας από τη θεωρία κατευθείαν στην εξάτμιση του profiler.
Δεν πρέπει να μείνετε σε αυτό, επειδή υπάρχουν πολλές άλλες εξίσου ενδιαφέρουσες προσεγγίσεις για τη σύνταξη λογισμικού δικτύου με διαφορετικά επίπεδα ευκολίας και ταχύτητας. Ενδιαφέρον, κατά τη γνώμη μου, οι σύνδεσμοι δίνονται παρακάτω.