Programación de red Linux: escriba su propio marco de servidor HTTP de alto rendimiento (2)

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

Implementación de modelo de E / S y modelo multiproceso

  • Varias consideraciones para el diseño multiproceso

En nuestro diseño, el subproceso del reactor principal es un subproceso aceptor. Una vez que se crea este subproceso, se bloqueará en el método de despacho de event_dispatcher en la forma de event_loop. De hecho, está esperando que ocurra un evento en el socket de escucha Una vez completada la conexión, se crearán el objeto de conexión tcp_connection y el objeto de canal.

Cuando el usuario espera utilizar varios subprocesos de sub-reactor, el subproceso principal creará varios subprocesos. Después de que se crea cada subproceso, se ejecuta e inicializa inmediatamente de acuerdo con la función de inicio especificada por el subproceso principal. La siguiente pregunta es , ¿cómo juzga el subproceso principal que el subproceso secundario ha terminado de inicializarse y de iniciarse, y continúa ejecutándolo? Este es un problema clave que debe resolverse.

Cuando se configuran varios subprocesos, los eventos de lectura y escritura correspondientes al socket conectado recién creado deben entregarse a un sub-reactor para su procesamiento. Por lo tanto, se toma un hilo de thread_pool para notificar a este hilo que se ha agregado un nuevo evento . Y este hilo probablemente se encuentre en la llamada de bloqueo de la distribución de eventos. Cómo coordinar los datos del hilo principal que se escribirán en el hilo secundario es otro problema clave que debe resolverse.

El subproceso secundario es un subproceso event_loop, que se bloquea en el envío. Una vez que ocurre un evento, buscará el channel_map, encontrará la función de procesamiento correspondiente y la ejecutará. Después de eso, agregará, eliminará o modificará los eventos pendientes y volverá a ingresar a la siguiente ronda de envío.

Se coloca una imagen en el manuscrito para ilustrar la relación de ejecución de los hilos:

                                           

Para facilitar su comprensión, he enumerado la implementación de la función correspondiente en otra figura.

                                           

  • El subproceso principal espera a que se inicialicen varios subprocesos del sub-reactor.

El subproceso principal debe esperar a que el subproceso complete la inicialización, es decir, debe obtener la retroalimentación de los datos correspondientes del subproceso, y la inicialización del subproceso también inicializa esta parte de los datos. De hecho, este es un problema de notificación de múltiples subprocesos. El método adoptado se mencionó anteriormente, utilizando las dos armas principales de mutex y condición.

El siguiente fragmento de código es la creación de subprocesos secundarios iniciados por el subproceso principal, llame a event_loop_thread_init para inicializar cada subproceso secundario, y luego llame a event_loop_thread_start para iniciar el subproceso secundario. Tenga en cuenta que si el tamaño del grupo de subprocesos especificado por la aplicación es 0, volverá directamente, de modo que el aceptador y los eventos de E / S se procesarán en el mismo subproceso principal, que degenera en un modo de reactor único.

//一定是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]);
    }
}

Veamos de nuevo el método event_loop_thread_start. Este método debe ser ejecutado por el hilo principal. Aquí utilicé pthread_create para crear un hilo secundario. Una vez creado el hilo secundario, event_loop_thread_run se ejecuta inmediatamente. Como veremos más adelante, event_loop_thread_run inicializa el hilo secundario. La parte más importante de event_loop_thread_start es el uso de pthread_mutex_lock y pthread_mutex_unlock para bloquear y desbloquear, y el uso de pthread_cond_wait para esperar la variable eventLoop en 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;
}

¿Por qué haces eso? Mire el código del hilo secundario y obtendrá una idea aproximada. La función de ejecución del subproceso event_loop_thread_run también se bloquea y luego inicializa el objeto event_loop. Cuando se completa la inicialización, se llama a la función pthread_cond_signal para notificar al hilo principal que está bloqueado en pthread_cond_wait en este momento. De esta manera, el hilo principal se despertará de la espera y el código debe ejecutarse antes. El subproceso secundario también entra en un cuerpo de ejecución de distribución de eventos de bucle infinito llamando a event_loop_run, esperando que ocurra el evento registrado en el reactor del subproceso secundario.

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

Se puede ver que la variable compartida por el subproceso principal y el subproceso secundario es el objeto eventLoop de cada event_loop_thread. Este objeto es NULL cuando se inicializa. Solo cuando se inicializa el subproceso secundario, se convierte en un valor no NULL. Este cambio es Una señal de que el subproceso secundario ha completado la inicialización también es una variable protegida por el semáforo. Mediante el uso de cerraduras y semáforos, se resuelve el problema de sincronización entre el subproceso principal y el subproceso. Cuando se inicializa el subproceso secundario, el subproceso principal continuará ejecutándose.

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 */
};

Puede preguntar, el subproceso principal está en bucle y espera a que cada subproceso complete la inicialización. Si ingresa al segundo bucle y espera a que el segundo subproceso complete su inicialización, ¿qué debo hacer? Tenga en cuenta que estamos bloqueando aquí, siempre que obtengamos este bloqueo y encontremos que el objeto eventLoop de event_loop_thread se ha convertido en un valor no NULL, podemos estar seguros de que el segundo hilo se ha inicializado y liberaremos el bloqueo directamente y proceda a ejecutarlo.

También puede preguntar, ¿necesito mantener ese bloqueo cuando se ejecuta pthread_cond_wait? Aquí, el hilo principal se dormirá inmediatamente después de llamar a la función pthread_cond_wait y liberará el bloqueo mutex que tenía. Y cuando el hilo principal regresa de pthread_cond_wait (esto se logra mediante el hilo secundario a través de la notificación pthread_cond_signal), el hilo retiene el bloqueo nuevamente.

  • Agregue eventos de socket conectados al hilo del sub-reactor

Como se mencionó anteriormente, el hilo principal es un hilo del reactor principal. Este hilo es responsable de detectar eventos en el conector de escucha. Cuando ocurre un evento, se ha establecido una conexión. Si tenemos varios hilos de sub-reactor, el resultado deseado es que los eventos de E / S relacionados con este socket conectado se entregan al subproceso secundario del sub-reactor para su detección. La ventaja de esto es que el reactor principal solo es responsable del establecimiento de enchufes de conexión y siempre puede mantener una eficiencia de procesamiento muy alta. En el caso de múltiples núcleos, múltiples sub-reactores pueden hacer un buen uso de las ventajas de múltiples procesamiento del núcleo.

Sabemos que el hilo del sub-reactor es un cuerpo de ejecución de bucle de eventos de bucle infinito. En el caso de que no ocurra ningún evento registrado, este hilo se bloquea en el despacho del event_dispatcher. Simplemente puede pensar en bloquear la llamada a la encuesta o epoll_wait. En este caso, ¿cómo puede el hilo principal entregar el conector conectado al hilo del sub-reactor?

Si podemos dejar que el hilo del sub-reactor regrese desde el envío del event_dispatcher, y luego dejar que el hilo del sub-reactor regrese para registrar el nuevo evento de socket conectado, este asunto está completo.

¿Cómo hacer que el hilo del sub-reactor regrese desde el envío del event_dispatcher? La respuesta es construir una palabra de descripción similar a una tubería, y dejar que event_dispatcher registre la palabra de descripción de la tubería. Cuando queremos que el sub-reactor se active, podemos enviar un carácter a la tubería.

En la función event_loop_init, se llama a la función socketpair para crear un par de sockets. El papel de este par de sockets es lo que acabo de decir. Al escribir en un extremo de este socket, el otro extremo puede percibir el evento de lectura . De hecho, la tubería en UNIX también se puede usar directamente aquí, y el efecto es el mismo.

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

Preste especial atención a este código en el manuscrito. Esto le dice a event_loop que el evento READ en el descriptor socketPair [1] está registrado. Si ocurre un evento READ, se llama a la función handleWakeup para completar el procesamiento del evento.

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

De hecho, esta función simplemente lee un carácter de la descripción socketPair [1] y no hace nada más. Su función principal es despertar a los subprocesos secundarios del bloqueo del envío.

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

Ahora, miremos hacia atrás nuevamente, si hay una nueva conexión, ¿cómo funciona el hilo principal? En handle_connection_established, el socket conectado se obtiene a través de la llamada accept, configúrelo como socket sin bloqueo (recuerde) y luego llame a thread_pool_get_loop para obtener un event_loop. La lógica de thread_pool_get_loop es muy simple: un hilo se selecciona en orden del grupo de hilos thread_pool para servir. Lo siguiente es la creación del objeto 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;
}

En el código que llama a tcp_connection_new para crear el objeto tcp_connection, puede ver que primero se crea un objeto de canal y que se registra el evento READ, y luego se llama al método event_loop_add_channel_event para agregar el objeto de canal al subproceso secundario.

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

Tenga en cuenta que las operaciones hasta ahora se han ejecutado en el hilo principal. El siguiente event_loop_do_channel_event no es una excepción, el siguiente comportamiento con el que espero que esté familiarizado, es decir, el desbloqueo. Si se puede adquirir el bloqueo, el hilo principal llamará a event_loop_channel_buffer_nolock para agregar el objeto de evento de canal que se procesará a los datos del hilo secundario. Todos los objetos de canal agregados se mantienen en la estructura de datos del subproceso secundario en forma de lista. La siguiente parte es el punto clave. Si el subproceso del bucle de eventos actual no está agregando el evento del canal, se llamará a la función event_loop_wakeup para activar el subproceso event_loop. La forma de despertar es muy simple, es decir, escribe un byte en el socketPair [0] justo ahora No olvides que event_loop ha registrado el evento legible de socketPair [1]. Si el hilo del bucle de eventos actual está agregando un evento de canal, llame directamente a event_loop_handle_pending_channel para procesar la lista de eventos de eventos del canal recién agregada.

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 se despierta, la función event_loop_handle_pending_channel se ejecutará a continuación. Puede ver que la función event_loop_handle_pending_channel también se llama después de salir del envío en el cuerpo del bucle.

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 función de event_loop_handle_pending_channel es recorrer la lista de eventos del canal pendiente en el bucle de eventos actual y asociarlos con event_dispatcher para modificar la colección de eventos de interés. Hay un punto que vale la pena señalar aquí, porque después de que el subproceso del bucle de eventos recibe el evento, volverá a llamar a la función de procesamiento de eventos, por lo que el código de la aplicación como onMessage también se ejecutará en el subproceso del bucle de eventos. Si la lógica de negocios aquí es demasiado complicada , hará que event_loop_handle_pending_channel se ejecute. El tiempo es tarde, lo que afecta la detección de E / S. Por lo tanto, es una práctica común aislar el subproceso de E / S del subproceso de lógica empresarial, dejar que el subproceso de E / S solo maneje la interacción de E / S y dejar que el subproceso de negocios maneje el negocio.

 

¡Aprenda lo nuevo revisando el pasado!

 

Supongo que te gusta

Origin blog.csdn.net/qq_24436765/article/details/104976313
Recomendado
Clasificación