どのようにイベント駆動プログラミングも動作しますか?


私はいつも、イベント駆動型プログラミングがどのように働いたのか不思議に思っていました.私はコールバックと約束の非同期の性質に混乱した.それは私にとっておもしろいsetTimeout or setInterval 実装されました!これは、C/C++のような別の言語では、コードのいくつかの領域でタイマーを常にチェックすることなく実装されるのは些細なことではありません.
ノードで.JPには、プログラマが書いたJavaScriptを実行するランタイムとJITコンパイラがあります.ランタイムは、同期C/C++が行う行ブロックの後に伝統的な行の操作を実行しません.代わりに、イベントループを持ち、操作を追加してプログラムの寿命を通してイベントループ上で実行します.イベントがI/Oを持っていて、ブロックされる必要があるならば、CPU Halting、Context Switchingの代わりにI/Oを完了するのを待ちます、ノード.JSランタイムは、ループ上の次のイベントを処理し続けます.以下に例を示します.
const fs = require('fs');

function hello_world(x) {
    console.log(`Hello World ${x}!`);
    fs.writeFile(`${x}.txt`, "hi", err => {
        if (err) {
            console.error(err);
        } else {
            console.log(`Finished writing to file ${x}`);
        }
    });
}

hello_world(1);
hello_world(2);
C/C++で書かれた同期版は、以下のように保証された出力順序を持ちます:
Hello World 1!
Finished writing to file 1
Hello World 2!
Finished writing to file 2
しかし、ノード.JSは、出力はおそらく以下のようになります.
Hello World 1!
Hello World 2!
Finished writing to file 1
Finished writing to file 2
それはほとんどノードのように見えます.JSランタイムは、I/O操作が起こっている間、CPU上の他の仕事をするのに十分スマートでした!フードの下、ノード.JSは追加hello_world(1) タスクキューに.実行中hello_world(1) , これは、いくつかのI/Oを行う必要があるので、後で説明するいくつかの魔法を行い、次の項目をタスクキューに実行する必要があることに気付きますhello_world(2) . 最後にノード.JSランタイムは、そのタスクキューに追加されたイベントを1.txt ファイルが完了し、メソッドコールを終了しますhello_world(1) .
ここで最も興味深い部分は、ノードがどのようなメカニズムです.JSはI/Oをブロックして、最初に完了する代わりに別のイベントを実行しますhello_world(1) . そして、どういうわけか、ランタイムは、ファイルが書き込まれた通知を取得し、fs.writeFile . すべてをこれ以上行うには、ノード.JSはLibuvと呼ばれる非同期I/Oライブラリを使用します.
ノード.JSはI/Oを行うラッパーとしてLibuvを使っています.時fs.writeFile が呼ばれると、リクエストはlibuvに送られ、ファイルにいくつかの内容を書き込むように伝えられます.最終的に、一度コンテンツが書き込まれると、Libuvはノードに通知を送信します.書き込み操作を指示するjsが完了し、コールバックを実行する必要がありますfs.writeFile . 以下にファイルI/Oを扱うときのlibuvの動作例を示します.
#include <uv.h>
#include <iostream>

uv_loop_t* loop;

void close_callback(uv_fs_t *close_request) {
    std::cout << "Finished closing file" << std::endl;
    int result = close_request->result;

    // Free the memory
    uv_fs_req_cleanup(close_request);

    if (result < 0) {
        std::cout << "There was an error closing the file" << std::endl;
        return;
    }
    std::cout << "Successfully wrote to the file" << std::endl;
}

void write_callback(uv_fs_t *write_request) {
    std::cout << "Wrote to file" << std::endl;
    int result = write_request->result;
    int data = *(int*) write_request->data;

    // Free the memory
    uv_fs_req_cleanup(write_request);

    if (result < 0) {
        std::cout << "There was an error writing to the file" << std::endl;
        return;
    }

    // Make sure to allocate on the heap since the stack will disappear with
    // an event loop model
    uv_fs_t* close_req = (uv_fs_t*) malloc(sizeof(uv_fs_t));
    uv_fs_close(loop, close_req, data, close_callback);
}
void open_callback(uv_fs_t *open_request) {
    std::cout << "Opened file" << std::endl;
    int result = open_request->result;

    // Free the memory
    uv_fs_req_cleanup(open_request);

    if (result < 0) {
        std::cout << "There was an error opening the file" << std::endl;
        return;
    }

    // Make sure to allocate on the heap since the stack will disappear with
    // an event loop model
    uv_fs_t* write_request = (uv_fs_t*) malloc(sizeof(uv_fs_t));
    write_request->data = (void*) malloc(sizeof(int));
    *((int*) write_request->data) = result;

    char str[] = "Hello World!\n";
    uv_buf_t buf = {str, sizeof(str)};

    uv_buf_t bufs[] = {buf};
    uv_fs_write(loop, write_request, result, bufs, 1 , -1, write_callback);
}

int main() {
    loop = uv_default_loop();

    uv_fs_t* open_request = (uv_fs_t*) malloc(sizeof(uv_fs_t));
    uv_fs_open(loop, open_request, "hello_world.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR, open_callback);

    uv_fs_t* open_request2 = (uv_fs_t*) malloc(sizeof(uv_fs_t));
    uv_fs_open(loop, open_request2, "hello_world2.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR, open_callback);

    // Run event loop
    return uv_run(loop, UV_RUN_DEFAULT);
}
この例では、イベントループとuv_run イベントを開始します.従来のC/C +同期スタイルプログラムでは、各I/O動作が長い時間がかかるので、シーケンシャルに実行して、長い時間を要すると思います.しかしながら、イベントループを持つ非同期I/OライブラリとしてLibuvを使用すると、I/Oブロックが問題にならなくなります.なぜなら、他のイベントがI/Oでブロックされている間、他の保留中のイベントを実行することができます.
Opened file
Opened file
Wrote to file
Wrote to file
Finished closing file
Succesfully wrote to the file
Finished closing file
Succesfully wrote to the file
ご覧のように、プログラムはオープンしていません.代わりに、各ファイルを開き、それらに書き込み、バッチでそれらを閉じます.これは、プログラムがI/Oを行うファイルを待っている間、別のイベントの操作を実行するためです.例えば、ファイルCUN_1をオープンするのを待っている間、syscallsに対してファイル2,1,3をオープンする.

でも.どのように、それはフードの下で働きますか?
Libuvで実装されている初期の推測は、I/O操作ごとに別々のスレッドを生成し、それをブロックすることです.I/O操作が完了すると、スレッドは終了し、メインLibuvスレッドに戻ります.メインLibuvスレッドは、ノードに通知します.I/O操作が完了したJS.しかし、これは非常に遅いです.あらゆるI/O要求のための新しい糸を生むことは、多くの追加CPUオーバーヘッドです!我々はより良いか?
私が持っているもう一つの考えは、興味のあるイベントを待つのを待って、興味のあるすべてのファイル記述子の上で常にSysscallを走らせることです.このデザインでは、1つのlibuvスレッドだけを必要とし、スレッドは常に準備ができているかどうかをチェックするために、すべてのファイル記述子をポーリングしてループを行います.このメソッドは、ファイルディスクリプタの数で線形O ( n )を拡大します.残念ながら、このメソッドも十分に高速ではありません.ノードを想像できます.JSのWebサーバを実行して、すべての繰り返しで5000ファイルディスクリプタをループして、読み取りまたは書き込みイベントをチェックします.
NGinxのような高性能Webサーバがこの問題を処理する方法(C 10 K問題)を少し詳しく理解した後、私はEPOLLに遭遇しました.epoll vs pollの利点は、epollが何らかのデータ更新を行うファイルディスクリプタを返すだけであるので、監視されたファイルディスクリプタの全てをスキャンする必要はない.これは世論調査よりもはるかによく、LibuvがLinux上での非同期I/Oを実装する方法です.
Linuxでは、epollは監視されたファイルディスクリプタのすべてのイベントに対して、プロセスごとにepollごとにepollを更新することで動作する.ユーザー空間プログラムが更新を持っているすべてのファイルディスクリプタを要求するとき、カーネルは既に更新されたファイルディスクリプタのこのリストを持っていて、単にそれをユーザ空間に転送しなければならない.世論調査では、カーネルはpollの実行中にそれらを反復処理することによってすべてのファイル記述子を問い合わせる必要があるので、pollから対照的です.

setTimerとsetIntervalについては、どのように実装されますか?
ここで、I/Oがシングルスレッドノードでどのように実装されているかを理解しています.SetTimerとsetIntervalのような機能をどのように動作しますか?これらはlibuvを使用しませんが、どのように動作するかを推測するのはかなり簡単です.なぜなら、私たちは今そのノードを知っているからです.JSはイベントドリブン言語であり、常にタスクキューからイベントをプルします.これは、ランタイムがすべてのイベントループ反復で期限切れになったかどうかを確認するために、すべてのタイマーまたは間隔をチェックします.それがあるならば、それはタイマーまたは間隔のためにコールバックを走らせます.そうでなければ、イベントループの次のフェーズにスキップします.すべてのタイマーと間隔が1つのループで処理されるというわけではないことに注意するのは重要です、ランタイムはしばしば各々の段階で処理するイベントの最大数を持ちます.

もっと好奇心がある?
あなたがより多くを学ぶことに興味があるならば、私に連絡してください[email protected] または私はdmのtwitter.チェックアウトマイblog .

その他の資源
https://nikhilm.github.io/uvbook/basics.html