「非同期」という用語は、JavaScript や AJAX とともに Web を席巻した Web 2.0 の波で広く普及しました。しかし、ほとんどの高級プログラミング言語では、非同期が発生することはまれです。 PHP はこの機能を最もよく体現しています。PHP は非同期をブロックするだけでなく、同期ブロック方式で実行されるマルチスレッドも提供しません。この利点は、プログラマがビジネス ロジックを順番に記述するのに役立ちますが、複雑なネットワーク アプリケーションでは、ブロッキングにより同時実行性の向上が妨げられます。
サーバー側では、I/O は非常に高価ですが、分散 I/O はさらに高価です。バックエンドがリソースに迅速に応答できる場合にのみ、フロントエンド エクスペリエンスが向上します。 Node.js は、主なプログラミング方法として非同期を使用する最初のプラットフォームであり、非同期 I/O とともに、Node の基調を形成するイベント駆動型およびシングルスレッドの設計コンセプトが採用されています。この記事では、Node が非同期 I/O を実装する方法を紹介します。
1. 基本概念
「非同期」と「ノンブロッキング」は、実際の効果という点では同じように聞こえますが、両方とも並列処理の目的を達成します。しかし、コンピュータ カーネル I/O の観点から見ると、方法はブロッキングとノンブロッキングの 2 つだけです。したがって、非同期/同期とブロッキング/非ブロッキングは実際には 2 つの異なるものです。
1.1 ブロッキング I/O とノンブロッキング I/O
ブロック I/O の特徴の 1 つは、呼び出し後、呼び出しが終了する前にシステム カーネル レベルですべての操作が完了するまで待機する必要があることです。ディスク上のファイルの読み取りを例にとると、この呼び出しは、システム カーネルがディスク シークを完了し、データを読み取り、メモリにデータをコピーした後に終了します。
I/O をブロックすると、CPU が I/O を待機することになり、待ち時間が無駄になり、CPU の処理能力を最大限に活用できなくなります。ノンブロッキング I/O の特徴は、呼び出し後すぐに戻ることです。戻った後は、CPU タイム スライスを他のトランザクションの処理に使用できます。完全な I/O が完了していないため、すぐに返されるのはビジネス層が期待するデータではなく、現在の呼び出しのステータスのみです。完全なデータを取得するには、アプリケーションは I/O 操作を繰り返し呼び出して、完了したかどうかを確認する (つまり、ポーリング) 必要があります。ポーリング手法には次のようなものがあります:
1.read: 呼び出しを繰り返して I/O ステータスを確認するのは、最も原始的でパフォーマンスが最も低い方法です
2.select: ファイル記述子のイベントステータスから判断する読み取りの改善。欠点は、ファイル記述子の最大数が制限されていることです
3.poll: 最大数制限を回避するためにリンクされたリストを使用して選択を改善しましたが、記述子が多い場合、パフォーマンスは依然として非常に低くなります
4.epoll: ポーリングに入るときに I/O イベントが検出されない場合、起動するイベントが発生するまでスリープ状態になります。これは、現在 Linux で最も効率的な I/O イベント通知メカニズムです
ポーリングは、完全なデータ取得を保証するためのノンブロッキング I/O のニーズを満たしますが、アプリケーションにとっては、I/O が返されるまで待つ必要があるため、依然として一種の同期としか見なされません。完全に。待機期間中、CPU はファイル記述子のステータスをトラバースするか、スリープしてイベントが発生するのを待つために使用されます。
1.2 理想と現実の非同期 I/O
完全な非同期 I/O は、アプリケーションがノンブロッキング呼び出しを開始し、I/O の終了後にシグナルまたはコールバックを通じてアプリケーションにデータを渡すだけで、ポーリングなしで次のタスクを直接処理できるときでなければなりません。完成しました。
実際には、非同期 I/O はオペレーティング システムごとに異なる実装になっています。たとえば、*nix プラットフォームはカスタム スレッド プールを使用し、Windows プラットフォームは IOCP モデルを使用します。 Node は、プラットフォーム互換性の判断をカプセル化するための抽象カプセル化レイヤーとして libuv を提供し、上位ノードと下位プラットフォームの非同期 I/O の実装が独立していることを保証します。さらに、Node がシングルスレッドであることは、Node 内で実際に I/O タスクを完了するスレッド プールが存在することを意味するだけであることを強調する必要があります。
2. ノードの非同期 I/O
2.1 イベントループ
ノードの実行モデルは実際にはイベント ループです。プロセスが開始されると、Node は無限ループを作成し、ループ本体の各実行が Tick になります。各 Tick プロセスは、処理を待機しているイベントがあるかどうかを確認し、存在する場合はイベントとそれに関連するコールバック関数を取得し、関連付けられているコールバック関数がある場合はそれらを実行して、次のループに入ります。処理するイベントがなくなった場合は、プロセスを終了します。
2.2 オブザーバー
各イベント ループには複数のオブザーバーがあり、これらのオブザーバーに問い合わせることで、処理するイベントがあるかどうかを判断できます。イベント ループは、典型的なプロデューサー/コンシューマー モデルです。 Node では、イベントは主にネットワーク リクエスト、ファイル I/O などから発生します。これらのイベントには、対応するネットワーク I/O オブザーバー、ファイル I/O オブザーバーなどがあり、イベント ループはオブザーバーからイベントを取得して処理します。
2.3 リクエストオブジェクト
呼び出しを開始する Javascript から I/O 操作を完了するカーネルまでの移行プロセスには、リクエスト オブジェクトと呼ばれる中間生成物が存在します。 Windows での最も単純な fs.open() メソッド (ファイルを開いて、指定されたパスとパラメーターに従ってファイル記述子を取得する) を例にとると、libuv を介して JS から組み込みモジュールにシステムを呼び出すと、実際には uv_fs_open( ) 方法。呼び出しプロセス中に、FSReqWrap リクエスト オブジェクトが作成されます。JS 層から渡されるパラメータとメソッドは、このリクエスト オブジェクトにカプセル化されます。最も重要なコールバック関数は、このオブジェクトの oncompete_sym 属性に設定されます。オブジェクトがラップされた後、FSReqWrap オブジェクトはスレッド プールにプッシュされ、実行を待ちます。
この時点で、JS 呼び出しはすぐに戻り、JS スレッドは引き続き後続の操作を実行できます。現在の I/O 操作はスレッド プールで実行を待機しており、非同期呼び出しの最初のフェーズが完了します。
2.4 実行コールバック
コールバック通知は、非同期 I/O の第 2 フェーズです。スレッド プール内の I/O 操作が呼び出された後、取得された結果が保存され、現在のオブジェクト操作が完了したことが IOCP に通知され、スレッドはスレッド プールに返されます。各 Tick の実行中、イベント ループの I/O オブザーバーは関連するメソッドを呼び出して、スレッド プールに完了したリクエストがあるかどうかを確認し、存在する場合はリクエスト オブジェクトが I/O のキューに追加されます。次に、それをイベントとして扱います。
3. 非 I/O 非同期 API
Node には、タスクを非同期で即座に実行するタイマー setTimeout()、setInterval()、process.nextTick()、setImmdiate() など、I/O とは関係のない非同期 API もいくつかあります。簡単な紹介です。
3.1 タイマー API
setTimeout() と setInterval() のブラウザ側 API は、その実装原理が非同期 I/O と似ていますが、I/O スレッド プールの参加を必要としません。タイマー API を呼び出して作成されたタイマーは、タイマー オブザーバー内の赤黒ツリーに挿入され、イベント ループの各 Tick によって繰り返しタイマー オブジェクトが赤黒ツリーから削除され、タイマーが超過したかどうかが確認されます。それを超えると、イベントが形成され、コールバック関数が直ちに実行されます。タイマーの主な問題は、そのタイミングが特に正確ではない (ミリ秒、許容範囲内) ことです。
3.2 即時非同期タスク実行 API
Node が登場する前は、多くの人がタスクを即座に非同期で実行するためにこれを呼び出していたかもしれません:
イベント ループの特性により、タイマーの精度は十分ではありません。タイマーを使用するには赤黒ツリーの使用が必要で、さまざまな操作の時間計算量は O(log(n)) です。 process.nextTick() メソッドはコールバック関数をキューに入れるだけで、Tick の次のラウンドで実行するために取り出します。複雑さは O(1) であり、より効率的です。
上記のメソッドと同様に、コールバック関数の実行を遅らせる setImmediate() メソッドもあります。ただし、イベント ループはオブザーバーを順番にチェックするため、前者の方が後者よりも優先されます。さらに、前者のコールバック関数は配列に格納され、Tick の各ラウンドで配列内のすべてのコールバック関数が実行され、後者の結果はリンクされたリストに格納され、1 回につき 1 つのコールバック関数のみが実行されます。ティックのラウンド。
4. イベント駆動型の高性能サーバー
前の例では、fs.open() を使用して、Node が非同期 I/O を実装する方法を説明しました。実際、Node は非同期 I/O を使用してネットワーク ソケットを処理します。これは、Node が Web サーバーを構築するための基礎でもあります。従来のサーバー モデルには次のものがあります:
1. 同期: 一度に 1 つのリクエストのみを処理でき、残りのリクエストは待機状態になります。
2. プロセスごと/リクエストごと: リクエストごとにプロセスを開始しますが、システム リソースは限られており、スケーラブルではありません
3. スレッドごと/リクエストごと: リクエストごとにスレッドを開始します。スレッドはプロセスよりも軽量ですが、各スレッドは一定量のメモリを占有します。大量の同時リクエストが到着すると、メモリはすぐに使い果たされます
有名な Apache はスレッドごと/リクエストごとの形式を使用しているため、高い同時実行性に対処することが困難です。ノードはイベント駆動型でリクエストを処理するため、スレッドの作成と破棄のオーバーヘッドを節約できます。同時に、タスクをスケジュールするときにオペレーティング システムのスレッドが少なくなるため、コンテキスト切り替えのコストも非常に低くなります。ノードは、接続数が多い場合でも、リクエストを順序立てて処理します。
有名なサーバーであるNginxもマルチスレッド方式を放棄し、Nodeと同じイベント駆動方式を採用しました。現在、Nginx は Apache に代わる大きな可能性を秘めています。 Nginx は純粋な C で書かれており、高いパフォーマンスを備えていますが、Web サーバー、リバース プロキシ、負荷分散などにのみ適しています。 NodeはNginxと同様の機能を構築でき、様々な特定業務にも対応でき、自身のパフォーマンスも良好です。実際のプロジェクトでは、それぞれの利点を組み合わせて、アプリケーションの最高のパフォーマンスを実現できます。