Linux網絡編程:自己動手寫高性能HTTP服務器框架(二)

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

I/O模型和多線程模型實現

  • 多線程設計的幾個考慮

在我們的設計中,main reactor 線程是一個 acceptor 線程,這個線程一旦創建,會以 event_loop 形式阻塞在 event_dispatcher 的 dispatch 方法上,實際上,它在等待監聽套接字上的事件發生,也就是已完成的連接,一旦有連接完成,就會創建出連接對象 tcp_connection,以及 channel 對象等。

當用戶期望使用多個 sub-reactor 子線程時,主線程會創建多個子線程,每個子線程在創建之後,按照主線程指定的啓動函數立即運行,並進行初始化。隨之而來的問題是,主線程如何判斷子線程已經完成初始化並啓動,繼續執行下去呢?這是一個需要解決的重點問題。

在設置了多個線程的情況下,需要將新創建的已連接套接字對應的讀寫事件交給一個 sub-reactor 線程處理。所以,這裏從 thread_pool 中取出一個線程,通知這個線程有新的事件加入。而這個線程很可能是處於事件分發的阻塞調用之中,如何協調主線程數據寫入給子線程,這是另一個需要解決的重點問題。

子線程是一個 event_loop 線程,它阻塞在 dispatch 上,一旦有事件發生,它就會查找 channel_map,找到對應的處理函數並執行它。之後它就會增加、刪除或修改 pending 事件,再次進入下一輪的 dispatch。

文稿中放置了一張圖,闡述了線程的運行關係:

                                           

爲了方便你理解,我把對應的函數實現列在了另外一張圖中。

                                           

  • 主線程等待多個 sub-reactor 子線程初始化完

主線程需要等待子線程完成初始化,也就是需要獲取子線程對應數據的反饋,而子線程初始化也是對這部分數據進行初始化,實際上這是一個多線程的通知問題。採用的做法在前面提到過,使用 mutex 和 condition 兩個主要武器。

下面這段代碼是主線程發起的子線程創建,調用 event_loop_thread_init 對每個子線程初始化,之後調用 event_loop_thread_start 來啓動子線程。注意,如果應用程序指定的線程池大小爲 0,則直接返回,這樣 acceptor 和 I/O 事件都會在同一個主線程裏處理,就退化爲單 reactor 模式。

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

我們再看一下 event_loop_thread_start 這個方法,這個方法一定是主線程運行的。這裏我使用了 pthread_create 創建了子線程,子線程一旦創建,立即執行 event_loop_thread_run,我們稍後將看到,event_loop_thread_run 進行了子線程的初始化工作。event_loop_thread_start 最重要的部分是使用了 pthread_mutex_lock 和 pthread_mutex_unlock 進行了加鎖和解鎖,並使用了 pthread_cond_wait 來守候 eventLoopThread 中的 eventLoop 的變量。

//由主線程調用,初始化一個子線程,並且讓子線程開始運行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;
}

爲什麼要這麼做呢?看一下子線程的代碼你就會大致明白。子線程執行函數 event_loop_thread_run 一上來也是進行了加鎖,之後初始化 event_loop 對象,當初始化完成之後,調用了 pthread_cond_signal 函數來通知此時阻塞在 pthread_cond_wait 上的主線程。這樣,主線程就會從 wait 中甦醒,代碼得以往下執行。子線程本身也通過調用 event_loop_run 進入了一個無限循環的事件分發執行體中,等待子線程 reactor 上註冊過的事件發生。

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

可以看到,這裏主線程和子線程共享的變量正是每個 event_loop_thread 的 eventLoop 對象,這個對象在初始化的時候爲 NULL,只有當子線程完成了初始化,才變成一個非 NULL 的值,這個變化是子線程完成初始化的標誌,也是信號量守護的變量。通過使用鎖和信號量,解決了主線程和子線程同步的問題。當子線程完成初始化之後,主線程纔會繼續往下執行。

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

你可能會問,主線程是循環在等待每個子線程完成初始化,如果進入第二個循環,等待第二個子線程完成初始化,而此時第二個子線程已經初始化完成了,該怎麼辦?注意我們這裏一上來是加鎖的,只要取得了這把鎖,同時發現 event_loop_thread 的 eventLoop 對象已經變成非 NULL 值,可以肯定第二個線程已經初始化,就直接釋放鎖往下執行了。

你可能還會問,在執行 pthread_cond_wait 的時候,需要持有那把鎖麼?這裏,父線程在調用 pthread_cond_wait 函數之後,會立即進入睡眠,並釋放持有的那把互斥鎖。而當父線程再從 pthread_cond_wait 返回時(這是子線程通過 pthread_cond_signal 通知達成的),該線程再次持有那把鎖。

  • 增加已連接套接字事件到 sub-reactor 線程中

前面提到,主線程是一個 main reactor 線程,這個線程負責檢測監聽套接字上的事件,當有事件發生時,也就是一個連接已完成建立,如果我們有多個 sub-reactor 子線程,我們期望的結果是,把這個已連接套接字相關的 I/O 事件交給 sub-reactor 子線程負責檢測。這樣的好處是,main reactor 只負責連接套接字的建立,可以一直維持在一個非常高的處理效率,在多核的情況下,多個 sub-reactor 可以很好地利用上多核處理的優勢。

我們知道,sub-reactor 線程是一個無限循環的 event loop 執行體,在沒有已註冊事件發生的情況下,這個線程阻塞在 event_dispatcher 的 dispatch 上。你可以簡單地認爲阻塞在 poll 調用或者 epoll_wait 上,這種情況下,主線程如何能把已連接套接字交給 sub-reactor 子線程呢?

如果我們能讓 sub-reactor 線程從 event_dispatcher 的 dispatch 上返回,再讓 sub-reactor 線程返回之後能夠把新的已連接套接字事件註冊上,這件事情就算完成了。

那如何讓 sub-reactor 線程從 event_dispatcher 的 dispatch 上返回呢?答案是構建一個類似管道一樣的描述字,讓 event_dispatcher 註冊該管道描述字,當我們想讓 sub-reactor 線程甦醒時,往管道上發送一個字符就可以了。

在 event_loop_init 函數裏,調用了 socketpair 函數創建了套接字對,這個套接字對的作用就是我剛剛說過的,往這個套接字的一端寫時,另外一端就可以感知到讀的事件。其實,這裏也可以直接使用 UNIX 上的 pipe 管道,作用是一樣的。

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

要特別注意的是文稿中的這句代碼,這告訴 event_loop 的,是註冊了 socketPair[1]描述字上的 READ 事件,如果有 READ 事件發生,就調用 handleWakeup 函數來完成事件處理。

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

事實上,這個函數就是簡單的從 socketPair[1]描述字上讀取了一個字符而已,除此之外,它什麼也沒幹。它的主要作用就是讓子線程從 dispatch 的阻塞中甦醒。

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

現在,我們再回過頭看看,如果有新的連接產生,主線程是怎麼操作的?在 handle_connection_established 中,通過 accept 調用獲取了已連接套接字,將其設置爲非阻塞套接字(切記),接下來調用 thread_pool_get_loop 獲取一個 event_loop。thread_pool_get_loop 的邏輯非常簡單,從 thread_pool 線程池中按照順序挑選出一個線程來服務。接下來是創建了 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;
}

在調用 tcp_connection_new 創建 tcp_connection 對象的代碼裏,可以看到先是創建了一個 channel 對象,並註冊了 READ 事件,之後調用 event_loop_add_channel_event 方法往子線程中增加 channel 對象。

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

請注意,到現在爲止的操作都是在主線程裏執行的。下面的 event_loop_do_channel_event 也不例外,接下來的行爲我期望你是熟悉的,那就是加解鎖。如果能夠獲取鎖,主線程就會調用 event_loop_channel_buffer_nolock 往子線程的數據中增加需要處理的 channel event 對象。所有增加的 channel 對象以列表的形式維護在子線程的數據結構中。接下來的部分是重點,如果當前增加 channel event 的不是當前 event loop 線程自己,就會調用 event_loop_wakeup 函數把 event_loop 子線程喚醒。喚醒的方法很簡單,就是往剛剛的 socketPair[0]上寫一個字節,別忘了,event_loop 已經註冊了 socketPair[1]的可讀事件。如果當前增加 channel event 的是當前 event loop 線程自己,則直接調用 event_loop_handle_pending_channel 處理新增加的 channel event 事件列表。

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

如果是 event_loop 被喚醒之後,接下來也會執行 event_loop_handle_pending_channel 函數。你可以看到在循環體內從 dispatch 退出之後,也調用了 event_loop_handle_pending_channel 函數。

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

event_loop_handle_pending_channel 函數的作用是遍歷當前 event loop 裏 pending 的 channel event 列表,將它們和 event_dispatcher 關聯起來,從而修改感興趣的事件集合。這裏有一個點值得注意,因爲 event loop 線程得到活動事件之後,會回調事件處理函數,這樣像 onMessage 等應用程序代碼也會在 event loop 線程執行,如果這裏的業務邏輯過於複雜,就會導致 event_loop_handle_pending_channel 執行的時間偏後,從而影響 I/O 的檢測。所以,將 I/O 線程和業務邏輯線程隔離,讓 I/O 線程只負責處理 I/O 交互,讓業務線程處理業務,是一個比較常見的做法。

 

溫故而知新 !

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章