Linuxネットワークプログラミング:独自の高性能HTTPサーバーフレームワークを作成する(2)

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

I / Oモデルとマルチスレッドモデルの実装

  • マルチスレッド設計に関するいくつかの考慮事項

私たちの設計では、メインのリアクタスレッドはアクセプタースレッドです。このスレッドが作成されると、event_loopの形式でevent_dispatcherのディスパッチメソッドでブロックされます。実際、リスニングソケットでイベントが発生するのを待機しています。接続が完了すると、接続オブジェクトtcp_connectionとチャネルオブジェクトが作成されます。

ユーザーが複数のサブリアクターサブスレッドを使用することを想定している場合、メインスレッドは複数のサブスレッドを作成します。各サブスレッドが作成されると、すぐに実行され、メインスレッドで指定された起動関数に従って初期化されます。次の質問は、メインスレッドが子スレッドの初期化と開始が終了したことをどのように判断し、実行を継続するかということです。これは解決する必要のある重要な問題です。

複数のスレッドが設定されている場合、新しく作成された接続されたソケットに対応する読み取りおよび書き込みイベントは、処理のためにサブリアクタースレッドに渡される必要があります。したがって、新しいイベントが追加されたことをこのスレッド通知するために、thread_poolからスレッドが取得されます。そして、このスレッドはおそらくイベント配布のブロッキング呼び出しにあります。子スレッドに書き込まれるメインスレッドデータを調整する方法は、解決する必要があるもう1つの重要な問題です。

子スレッドはevent_loopスレッドであり、ディスパッチ時にブロックされます。イベントが発生すると、channel_mapを検索し、対応する処理関数を見つけて実行します。その後、保留中のイベントを追加、削除、または変更し、次のディスパッチラウンドに入ります。

スレッドの実行関係を説明するために、写真が原稿に配置されています。

                                           

理解を容易にするために、対応する関数の実装を別の図に示しました。

                                           

  • メインスレッドは、複数のサブリアクターサブスレッドが初期化されるのを待ちます

メインスレッドは、サブスレッドが初期化を完了するのを待つ必要があります。つまり、サブスレッドの対応するデータのフィードバックを取得する必要があり、サブスレッドの初期化もデータのこの部分を初期化します。実際、これはマルチスレッドの通知の問題です。採用された方法は、ミューテックスとコンディションの2つの主要な武器を使用して前述しました。

次のコードは、メインスレッドによって開始される子スレッドの作成です。event_loop_thread_initを呼び出して各子スレッドを初期化し、次にevent_loop_thread_startを呼び出して子スレッドを開始します。アプリケーションで指定されたスレッドプールサイズが0の場合、直接返されるため、アクセプターイベントとI / Oイベントは同じメインスレッドで処理され、単一のリアクターモードに縮退することに注意してください。

//一定是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の使用、およびeventLoopThreadのeventLoop変数を待機するためのpthread_cond_waitの使用です。

//由主线程调用,初始化一个子线程,并且让子线程开始运行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でブロックされているメインスレッドに通知されます。このようにして、メインスレッドは待機からウェイクアップし、コードは前に実行する必要があります。子スレッド自体も、event_loop_runを呼び出して無限ループイベント配布実行本体に入り、子スレッドリアクタに登録されているイベントが発生するのを待ちます。

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

メインスレッドはループしていて、各サブスレッドが初期化を完了するのを待っています。2番目のループに入り、2番目のサブスレッドが初期化を完了するのを待っている場合は、どうすればよいですか。このロックを取得し、event_loop_threadのeventLoopオブジェクトがNULL以外の値になっていることがわかっている限り、ここでロックしていることに注意してください。2番目のスレッドが初期化されていることを確認でき、ロックを直接解放します。実行に進みます。

また、pthread_cond_waitが実行されるときに、そのロックを保持する必要がありますか?ここで、親スレッドはpthread_cond_wait関数を呼び出した直後にスリープ状態になり、保持していたミューテックスロックを解放します。そして、親スレッドがpthread_cond_waitから戻ると(これは、pthread_cond_signal通知を介して子スレッドによって実現されます)、スレッドは再びロックを保持します。

  • 接続されたソケットイベントをサブリアクタースレッドに追加します

前述のように、メインスレッドはメインリアクタスレッドです。このスレッドは、リスニングソケットでのイベントの検出を担当します。イベントが発生すると、接続が確立されます。複数のサブリアクタスレッドがある場合、望ましい結果は次のようになります。この接続されたソケットに関連するI / Oイベントは、検出のためにサブリアクターの子スレッドに渡されます。これの利点は、メインリアクターが接続ソケットの確立のみを担当し、常に非常に高い処理効率を維持できることです。マルチコアの場合、複数のサブリアクターがマルチリアクターの利点を十分に活用できます。コア処理。

サブリアクタスレッドは無限ループイベントループ実行本体であることがわかっています。登録されたイベントが発生しない場合、このスレッドはevent_dispatcherのディスパッチでブロックされます。ポーリング呼び出しまたはepoll_waitでのブロックを簡単に考えることができます。この場合、メインスレッドは接続されたソケットをサブリアクタースレッドにどのように引き渡すことができますか?

event_dispatcherのディスパッチからサブリアクタースレッドを戻してから、サブリアクタースレッドを戻して新しい接続ソケットイベントを登録できれば、この問題は完了です。

event_dispatcherのディスパッチからサブリアクタースレッドを返すにはどうすればよいですか?答えは、パイプラインに似た説明ワードを作成し、event_dispatcherにパイプライン説明ワードを登録させることです。サブリアクタースレッドをウェイクアップさせたい場合は、パイプラインに文字を送信できます。

event_loop_init関数では、socketpair関数が呼び出されて、ソケットペアが作成されます。このソケットペアの役割は、先ほど述べたとおりです。このソケットの一方の端に書き込むと、もう一方の端は読み取りイベントを認識できます。実際、UNIXのパイプもここで直接使用でき、効果は同じです。

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

原稿のこのコードに特に注意してください。これにより、socketPair [1]記述子のREADイベントが登録されていることがevent_loopに通知されます。READイベントが発生すると、handleWakeup関数が呼び出されてイベント処理が完了します。

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

実際、この関数は、socketPair [1]の説明から文字を読み取るだけで、他には何もしません。その主な機能は、ディスパッチのブロックから子スレッドをウェイクアップすることです。

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オブジェクトを作成するコードでは、最初にチャネルオブジェクトが作成され、READイベントが登録されてから、event_loop_add_channel_eventメソッドが呼び出されてチャネルオブジェクトが子スレッドに追加されていることがわかります。

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を呼び出して、処理するチャネルイベントオブジェクトを子スレッドのデータに追加します。追加されたすべてのチャネルオブジェクトは、リストの形式で子スレッドのデータ構造に保持されます。次の部分は重要なポイントです。現在のイベントループスレッドがチャネルイベントを追加していない場合、event_loop_wakeup関数が呼び出されてevent_loopサブスレッドがウェイクアップされます。ウェイクアップする方法は非常に簡単です。つまり、今すぐsocketPair [0]にバイトを書き込みます。event_loopがsocketPair [1]の読み取り可能なイベントを登録したことを忘れないでください。現在のイベントループスレッドがチャネルイベントを追加している場合は、event_loop_handle_pending_channelを直接呼び出して、新しく追加されたチャネルイベントイベントリストを処理します。

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関数が実行されます。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_dispatcherに関連付けて、対象のイベントのコレクションを変更することです。ここで注目すべき点が1つあります。これは、イベントループスレッドがイベントを取得した後、イベント処理関数を呼び出すため、onMessageなどのアプリケーションコードもイベントループスレッドで実行されるためです。ここでのビジネスロジックが複雑すぎる場合、event_loop_handle_pending_channelが実行されます。時間が遅れ、I / O検出に影響します。したがって、I / Oスレッドをビジネスロジックスレッドから分離し、I / OスレッドにI / Oインタラクションのみを処理させ、ビジネススレッドにビジネスを処理させるのが一般的な方法です。

 

過去を振り返って新しいことを学びましょう!

 

おすすめ

転載: blog.csdn.net/qq_24436765/article/details/104976313