Programmation réseau Linux: écrivez votre propre framework de serveur HTTP hautes performances (2)

github :https://github.com/froghui/yolanda

Modèle d'E / S et implémentation de modèle multi-thread

  • Plusieurs considérations pour la conception multithread

Dans notre conception, le thread du réacteur principal est un thread accepteur. Une fois ce thread créé, il sera bloqué sur la méthode de répartition de event_dispatcher sous la forme event_loop. En fait, il attend qu'un événement se produise sur le socket d'écoute Une fois la connexion établie, l'objet de connexion tcp_connection et l'objet de canal seront créés.

Lorsque l'utilisateur prévoit d'utiliser plusieurs sous-threads de sous-réacteur, le thread principal crée plusieurs sous-threads. Une fois que chaque sous-thread est créé, il s'exécute et s'initialise immédiatement en fonction de la fonction de démarrage spécifiée par le thread principal. La question qui s'ensuit est la suivante : comment le thread principal juge-t-il que le thread enfant a terminé son initialisation et son démarrage et continue de l'exécuter? C'est un problème clé qui doit être résolu.

Lorsque plusieurs threads sont définis, les événements de lecture et d'écriture correspondant au socket connecté nouvellement créé doivent être transférés à un thread de sous-réacteur pour traitement. Par conséquent, un thread est extrait de thread_pool pour notifier ce thread qu'un nouvel événement a été ajouté . Et ce thread est probablement dans l'appel bloquant de la distribution des événements. Comment coordonner les données du thread principal à écrire sur le thread enfant est un autre problème clé qui doit être résolu.

Le thread enfant est un thread event_loop, qui est bloqué lors de la distribution. Une fois qu'un événement se produit, il recherche le channel_map, trouve la fonction de traitement correspondante et l'exécute. Après cela, il ajoutera, supprimera ou modifiera les événements en attente et entrera à nouveau dans la prochaine ronde d'expédition.

Une image est placée dans le manuscrit pour illustrer la relation courante des fils:

                                           

Afin de faciliter votre compréhension, j'ai répertorié l'implémentation de la fonction correspondante dans une autre figure.

                                           

  • Le thread principal attend que plusieurs sous-threads de sous-réacteur soient initialisés

Le thread principal doit attendre que le sous-thread termine l'initialisation, c'est-à-dire qu'il doit obtenir le retour d'informations sur les données correspondantes du sous-thread, et l'initialisation du sous-thread initialise également cette partie des données. en fait, il s'agit d'un problème de notification multi-thread. La méthode adoptée a été mentionnée précédemment, utilisant les deux principales armes du mutex et de la condition.

Le morceau de code suivant est la création de threads enfants lancés par le thread principal, appelez event_loop_thread_init pour initialiser chaque thread enfant, puis appelez event_loop_thread_start pour démarrer le thread enfant. Notez que si la taille du pool de threads spécifiée par l'application est 0, elle retournera directement, de sorte que l'accepteur et les événements d'E / S seront traités dans le même thread principal, qui dégénère en un mode réacteur unique.

//一定是main thread发起
void thread_pool_start(struct thread_pool *threadPool) {
    assert(!threadPool->started);
    assertInSameThread(threadPool->mainLoop);

    threadPool->started = 1;
    void *tmp;
    if (threadPool->thread_number <= 0) {
        return;
    }

    threadPool->eventLoopThreads = malloc(threadPool->thread_number * sizeof(struct event_loop_thread));
    for (int i = 0; i < threadPool->thread_number; ++i) {
        event_loop_thread_init(&threadPool->eventLoopThreads[i], i);
        event_loop_thread_start(&threadPool->eventLoopThreads[i]);
    }
}

Regardons à nouveau la méthode event_loop_thread_start. Cette méthode doit être exécutée par le thread principal. Ici, j'ai utilisé pthread_create pour créer un thread enfant. Une fois le thread enfant créé, event_loop_thread_run est exécuté immédiatement. Comme nous le verrons plus tard, event_loop_thread_run initialise le thread enfant. La partie la plus importante de event_loop_thread_start est l'utilisation de pthread_mutex_lock et pthread_mutex_unlock pour le verrouillage et le déverrouillage, et l'utilisation de pthread_cond_wait pour attendre la variable eventLoop dans eventLoopThread.

//由主线程调用,初始化一个子线程,并且让子线程开始运行event_loop
struct event_loop *event_loop_thread_start(struct event_loop_thread *eventLoopThread) {
    pthread_create(&eventLoopThread->thread_tid, NULL, &event_loop_thread_run, eventLoopThread);

    assert(pthread_mutex_lock(&eventLoopThread->mutex) == 0);

    while (eventLoopThread->eventLoop == NULL) {
        assert(pthread_cond_wait(&eventLoopThread->cond, &eventLoopThread->mutex) == 0);
    }
    assert(pthread_mutex_unlock(&eventLoopThread->mutex) == 0);

    yolanda_msgx("event loop thread started, %s", eventLoopThread->thread_name);
    return eventLoopThread->eventLoop;
}

Pourquoi fais-tu cela? Regardez le code du thread enfant et vous aurez une idée approximative. La fonction d'exécution de sous-thread event_loop_thread_run est également verrouillée, puis initialise l'objet event_loop. Lorsque l'initialisation est terminée, la fonction pthread_cond_signal est appelée pour notifier le thread principal qui est bloqué sur pthread_cond_wait à ce moment. De cette façon, le thread principal se réveillera après l'attente, et le code doit être exécuté avant. Le thread enfant lui-même entre également dans un corps d'exécution de distribution d'événements en boucle infinie en appelant event_loop_run, en attendant que l'événement enregistré sur le réacteur de thread enfant se produise.

void *event_loop_thread_run(void *arg) {
    struct event_loop_thread *eventLoopThread = (struct event_loop_thread *) arg;

    pthread_mutex_lock(&eventLoopThread->mutex);

    // 初始化化event loop,之后通知主线程
    eventLoopThread->eventLoop = event_loop_init();
    yolanda_msgx("event loop thread init and signal, %s", eventLoopThread->thread_name);
    pthread_cond_signal(&eventLoopThread->cond);

    pthread_mutex_unlock(&eventLoopThread->mutex);

    //子线程event loop run
    eventLoopThread->eventLoop->thread_name = eventLoopThread->thread_name;
    event_loop_run(eventLoopThread->eventLoop);
}

On peut voir que la variable partagée par le thread principal et le thread enfant est l'objet eventLoop de chaque event_loop_thread. Cet objet est NULL lorsqu'il est initialisé. Uniquement lorsque le thread enfant est initialisé, il devient une valeur non NULL. Cette modification est Un signe que le thread enfant a terminé l'initialisation est également une variable gardée par le sémaphore. En utilisant des verrous et des sémaphores, le problème de la synchronisation entre le thread principal et le sous-thread est résolu. Lorsque le thread enfant est initialisé, le thread principal continuera à s'exécuter.

struct event_loop_thread {
    struct event_loop *eventLoop;
    pthread_t thread_tid;        /* thread ID */
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    char * thread_name;
    long thread_count;    /* # connections handled */
};

Vous pouvez demander, le thread principal est en boucle et attend que chaque sous-thread pour terminer l'initialisation. S'il entre dans la deuxième boucle et attend que le second sous-thread pour terminer son initialisation, que dois-je faire? Notez que nous nous bloquons ici, tant que nous obtenons ce verrou et constatons que l'objet eventLoop de event_loop_thread est devenu une valeur non NULL, nous pouvons être sûrs que le deuxième thread a été initialisé, et nous libérerons le verrou directement et procédez à son exécution.

Vous pouvez également demander, dois-je maintenir ce verrou lorsque pthread_cond_wait est exécuté? Ici, le thread parent se met en veille immédiatement après avoir appelé la fonction pthread_cond_wait et libère le verrou mutex qu'il détenait. Et lorsque le thread parent revient de pthread_cond_wait (ceci est réalisé par le thread enfant via la notification pthread_cond_signal), le thread retient à nouveau le verrou.

  • Ajouter des événements de socket connecté au thread de sous-réacteur

Comme mentionné précédemment, le thread principal est un thread de réacteur principal. Ce thread est responsable de la détection des événements sur le socket d'écoute. Lorsqu'un événement se produit, une connexion a été établie. Si nous avons plusieurs threads de sous-réacteur, nous Le résultat souhaité est que les événements d'E / S liés à cette prise connectée sont transmis au thread enfant du sous-réacteur pour détection. L'avantage de ceci est que le réacteur principal n'est responsable que de la mise en place des prises de connexion et peut toujours maintenir une efficacité de traitement très élevée.Dans le cas de multicœurs, plusieurs sous-réacteurs peuvent faire bon usage des avantages du multi- traitement de base.

On sait que le thread de sous-réacteur est un corps d'exécution de boucle d'événement en boucle infinie.Dans le cas où aucun événement enregistré ne se produit, ce thread est bloqué lors de l'envoi du event_dispatcher. Vous pouvez simplement penser au blocage lors d'un appel d'interrogation ou d'epoll_wait. Dans ce cas, comment le thread principal peut-il remettre le socket connecté au thread du sous-réacteur?

Si nous pouvons laisser le thread du sous-réacteur revenir de l'envoi de event_dispatcher, puis laisser le thread du sous-réacteur revenir pour enregistrer le nouvel événement de socket connecté, cette question est terminée.

Comment faire revenir le thread du sous-réacteur à partir de l'envoi du event_dispatcher? La réponse est de construire un mot de description similaire à un pipeline, et de laisser event_dispatcher enregistrer le mot de description du pipeline. Lorsque nous voulons que le thread du sous-réacteur se réveille, nous pouvons envoyer un caractère au pipeline.

Dans la fonction event_loop_init, la fonction socketpair est appelée pour créer une paire de sockets. Le rôle de cette paire de sockets est ce que je viens de dire. Lors de l'écriture à une extrémité de cette socket, l'autre extrémité peut percevoir l'événement de lecture . En fait, le tube sous UNIX peut également être utilisé directement ici, et l'effet est le même.

struct event_loop *event_loop_init() {
    ...
    //add the socketfd to event 这里创建的是套接字对,目的是为了唤醒子线程
    eventLoop->owner_thread_id = pthread_self();
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, eventLoop->socketPair) < 0) {
        LOG_ERR("socketpair set fialed");
    }
    eventLoop->is_handle_pending = 0;
    eventLoop->pending_head = NULL;
    eventLoop->pending_tail = NULL;
    eventLoop->thread_name = "main thread";

    struct channel *channel = channel_new(eventLoop->socketPair[1], EVENT_READ, handleWakeup, NULL, eventLoop);
    event_loop_add_channel_event(eventLoop, eventLoop->socketPair[0], channel);

    return eventLoop;
}

Faites particulièrement attention à ce code dans le manuscrit. Cela indique à event_loop que l'événement READ sur le descripteur socketPair [1] est enregistré. Si un événement READ se produit, la fonction handleWakeup est appelée pour terminer le traitement de l'événement.

struct channel *channel = channel_new(eventLoop->socketPair[1], EVENT_READ, handleWakeup, NULL, eventLoop);

En fait, cette fonction lit simplement un caractère dans la description de socketPair [1], et elle ne fait rien d'autre. Sa fonction principale est de réveiller les threads enfants du blocage de la distribution.

int handleWakeup(void * data) {
    struct event_loop *eventLoop = (struct event_loop *) data;
    char one;
    ssize_t n = read(eventLoop->socketPair[1], &one, sizeof one);
    if (n != sizeof one) {
        LOG_ERR("handleWakeup  failed");
    }
    yolanda_msgx("wakeup, %s", eventLoop->thread_name);
}

Maintenant, revenons en arrière, s'il y a une nouvelle connexion, comment fonctionne le thread principal? Dans handle_connection_established, le socket connecté est obtenu via l'appel d'acceptation, définissez-le comme un socket non bloquant (rappelez-vous), puis appelez thread_pool_get_loop pour obtenir un event_loop. La logique de thread_pool_get_loop est très simple: un thread est sélectionné dans l'ordre dans le pool de threads thread_pool à servir. Vient ensuite la création de l'objet tcp_connection.

//处理连接已建立的回调函数
int handle_connection_established(void *data) {
    struct TCPserver *tcpServer = (struct TCPserver *) data;
    struct acceptor *acceptor = tcpServer->acceptor;
    int listenfd = acceptor->listen_fd;

    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    //获取这个已建立的套集字,设置为非阻塞套集字
    int connected_fd = accept(listenfd, (struct sockaddr *) &client_addr, &client_len);
    make_nonblocking(connected_fd);

    yolanda_msgx("new connection established, socket == %d", connected_fd);

    //从线程池里选择一个eventloop来服务这个新的连接套接字
    struct event_loop *eventLoop = thread_pool_get_loop(tcpServer->threadPool);

    // 为这个新建立套接字创建一个tcp_connection对象,并把应用程序的callback函数设置给这个tcp_connection对象
    struct tcp_connection *tcpConnection = tcp_connection_new(connected_fd, eventLoop,tcpServer->connectionCompletedCallBack,tcpServer->connectionClosedCallBack,tcpServer->messageCallBack,tcpServer->writeCompletedCallBack);
    //callback内部使用
    if (tcpServer->data != NULL) {
        tcpConnection->data = tcpServer->data;
    }
    return 0;
}

Dans le code qui appelle tcp_connection_new pour créer l'objet tcp_connection, vous pouvez voir qu'un objet channel est créé en premier et que l'événement READ est enregistré, puis que la méthode event_loop_add_channel_event est appelée pour ajouter l'objet channel au thread enfant.

tcp_connection_new(int connected_fd, struct event_loop *eventLoop,
                   connection_completed_call_back connectionCompletedCallBack,
                   connection_closed_call_back connectionClosedCallBack,
                   message_call_back messageCallBack, write_completed_call_back writeCompletedCallBack) {
    ...
    //为新的连接对象创建可读事件
    struct channel *channel1 = channel_new(connected_fd, EVENT_READ, handle_read, handle_write, tcpConnection);
    tcpConnection->channel = channel1;

    //完成对connectionCompleted的函数回调
    if (tcpConnection->connectionCompletedCallBack != NULL) {
        tcpConnection->connectionCompletedCallBack(tcpConnection);
    }
  
    //把该套集字对应的channel对象注册到event_loop事件分发器上
    event_loop_add_channel_event(tcpConnection->eventLoop, connected_fd, tcpConnection->channel);
    return tcpConnection;
}

Veuillez noter que les opérations jusqu'à présent ont été exécutées dans le thread principal. L'événement event_loop_do_channel_event suivant ne fait pas exception, le prochain comportement que j'attends que vous soyez familier, c'est-à-dire le déverrouillage. Si le verrou peut être acquis, le thread principal appellera event_loop_channel_buffer_nolock pour ajouter l'objet événement de canal à traiter aux données du thread enfant. Tous les objets de canal ajoutés sont conservés dans la structure de données du thread enfant sous la forme d'une liste. La partie suivante est le point clé. Si le thread de la boucle d'événements en cours n'ajoute pas l'événement de canal, la fonction event_loop_wakeup sera appelée pour réveiller le sous-thread event_loop. La façon de se réveiller est très simple, c'est-à-dire d'écrire un octet dans le socketPair [0] tout de suite. N'oubliez pas, event_loop a enregistré l'événement lisible de socketPair [1]. Si le thread de la boucle d'événements en cours ajoute un événement de canal, appelez directement event_loop_handle_pending_channel pour traiter la liste d'événements de canal nouvellement ajoutée.

int event_loop_do_channel_event(struct event_loop *eventLoop, int fd, struct channel *channel1, int type) {
    //get the lock
    pthread_mutex_lock(&eventLoop->mutex);
    assert(eventLoop->is_handle_pending == 0);
    //往该线程的channel列表里增加新的channel
    event_loop_channel_buffer_nolock(eventLoop, fd, channel1, type);
    //release the lock
    pthread_mutex_unlock(&eventLoop->mutex);
    //如果是主线程发起操作,则调用event_loop_wakeup唤醒子线程
    if (!isInSameThread(eventLoop)) {
        event_loop_wakeup(eventLoop);
    } else {
        //如果是子线程自己,则直接可以操作
        event_loop_handle_pending_channel(eventLoop);
    }
    return 0;
}

Si event_loop est réveillé, la fonction event_loop_handle_pending_channel sera exécutée ensuite. Vous pouvez voir que la fonction event_loop_handle_pending_channel est également appelée après avoir quitté la distribution dans le corps de la boucle.

int event_loop_run(struct event_loop *eventLoop) {
    assert(eventLoop != NULL);

    struct event_dispatcher *dispatcher = eventLoop->eventDispatcher;
    if (eventLoop->owner_thread_id != pthread_self()) {
        exit(1);
    }

    yolanda_msgx("event loop run, %s", eventLoop->thread_name);
    struct timeval timeval;
    timeval.tv_sec = 1;

    while (!eventLoop->quit) {
        //block here to wait I/O event, and get active channels
        dispatcher->dispatch(eventLoop, &timeval);

        //这里处理pending channel,如果是子线程被唤醒,这个部分也会立即执行到
        event_loop_handle_pending_channel(eventLoop);
    }
    yolanda_msgx("event loop end, %s", eventLoop->thread_name);
    return 0;
}

La fonction de event_loop_handle_pending_channel est de parcourir la liste d'événements de canal en attente dans la boucle d'événements en cours et de les associer à event_dispatcher pour modifier la collection d'événements d'intérêt. Il y a un point à noter ici, car une fois que le thread de la boucle d'événements obtient l'événement, il rappellera la fonction de traitement des événements, donc le code d'application comme onMessage sera également exécuté dans le thread de la boucle d'événements. Si la logique métier ici est trop compliquée , cela provoquera l'exécution de event_loop_handle_pending_channel. L'heure est en retard, ce qui affecte la détection d'E / S. Par conséquent, il est courant d'isoler le thread d'E / S du thread de logique métier, de laisser le thread d'E / S gérer uniquement l'interaction d'E / S et de laisser le thread métier gérer l'entreprise.

 

Apprenez le nouveau en revoyant le passé!

 

Je suppose que tu aimes

Origine blog.csdn.net/qq_24436765/article/details/104976313
conseillé
Classement