コンピュータ サイエンスにおいて、非同期 I/O (非シーケンシャル I/Oとも呼ばれる) は、I/O 操作が完了する前に他の処理を続行できる入出力処理の形式です。Windows API で非同期 I/O に使用される名前は、オーバーラップ I/Oです。
コンピュータの入出力(I/O) 操作は、データの処理に比べて非常に遅くなることがあります。I/O デバイスには、読み取りまたは書き込みを行うトラックを探すハード ドライブなど、物理的に移動する必要がある機械装置が組み込まれている場合があります。これは、電流の切り替えよりも桁違いに遅くなることがよくあります。たとえば、実行に 10 ミリ秒かかるディスク操作中に、1ギガヘルツでクロックされるプロセッサは、1,000 万回の命令処理サイクルを実行できます。
I/O に対する単純なアプローチは、アクセスを開始して完了するまで待つことです。しかし、同期 I/Oまたはブロッキング I/Oと呼ばれるこのようなアプローチでは、通信が進行中にプログラムの進行がブロックされ、システム リソースがアイドル状態になります。プログラムが多くの I/O 操作を行う場合 (主にまたは大きくユーザー入力に依存するプログラムなど)、プロセッサは I/O 操作が完了するのを待機して、ほぼすべての時間をアイドル状態に費やす可能性があります。
あるいは、通信を開始してから、I/O の完了を必要としない処理を実行することもできます。このアプローチは、非同期入出力と呼ばれます。I/O の完了に依存するタスク (入力値の使用と、書き込み操作が完了したことを確認するための重要な操作の両方を含む) は、I/O 操作が完了するまで待機する必要があるため、依然としてブロックされますが、I/O 操作に依存しない他の処理は続行できます。
多くのオペレーティング システム関数は、さまざまなレベルで非同期 I/O を実装するために存在します。実際、最も基本的なオペレーティング システムを除くすべてのオペレーティング システムの主な機能の 1 つは、少なくとも何らかの基本的な非同期 I/O を実行することですが、これはユーザーやプログラマーにとって特に明らかではないかもしれません。最も単純なソフトウェア ソリューションでは、ハードウェア デバイスの状態が一定間隔でポーリングされ、デバイスが次の操作の準備ができているかどうかを検出します。(たとえば、CP/Mオペレーティング システムはこのように構築されました。そのシステム コールセマンティクスでは、これよりも複雑な I/O 構造は必要ありませんでしたが、ほとんどの実装はより複雑で、したがってより効率的でした。)ダイレクト メモリ アクセス(DMA) により、ポーリング ベースのシステムの効率が大幅に向上し、ハードウェア割り込みによりポーリングがまったく不要になります。マルチタスクオペレーティング システムは、ハードウェア割り込みによって提供される機能を活用できますが、割り込み処理の複雑さはユーザーからは見えません。スプーリングは、非同期 I/O を活用するために設計された最初のマルチタスク形式の 1 つでした。最後に、ユーザー プロセス内のマルチスレッドと明示的な非同期 I/O API を使用すると、ソフトウェアの複雑さが増す代わりに、非同期 I/O をさらに活用できます。
非同期 I/O は、エネルギー効率と、場合によってはスループットを向上させるために使用されます。ただし、場合によっては、レイテンシとスループットに悪影響を与える可能性があります。
フォーム
I/O の形式と POSIX 関数の例:
あらゆる形式の非同期 I/O により、アプリケーションは潜在的なリソース競合やそれに伴う障害に晒される可能性があります。これを防ぐには、慎重なプログラミング (多くの場合、相互排他、セマフォなどを使用) が必要です。
アプリケーションに非同期 I/O を公開する場合、実装にはいくつかの大まかなクラスがあります。アプリケーションに提供されるAPIの形式は、オペレーティング システムが実際に提供するメカニズムと必ずしも一致しません。エミュレーションは可能です。さらに、アプリケーションのニーズやプログラマーの希望に応じて、1 つのアプリケーションで複数のメソッドが使用される場合があります。多くのオペレーティング システムはこれらのメカニズムを複数提供しており、すべてのメカニズムを提供するオペレーティング システムもあります。
プロセス
初期の Unix で利用可能。マルチタスクオペレーティング システムでは、処理は複数のプロセスに分散され、プロセスは独立して実行され、独自のメモリを持ち、独自の I/O フローを処理します。これらのフローは通常、パイプラインで接続されます。プロセスの作成と維持にはかなりコストがかかるため[引用が必要]、このソリューションはプロセス セットが小さく、比較的安定している場合にのみ有効です。また、個々のプロセスは、互いの I/O を処理することを除いて、独立して動作できることを前提としています。他の方法で通信する必要がある場合、それらの調整は困難になる可能性があります。[引用が必要]
このアプローチの拡張はデータフロー プログラミングであり、これにより、パイプがサポートするチェーンだけでなく、より複雑なネットワークが可能になります。
投票
バリエーション:
- まだできない場合はエラー(後で再発行)
- ブロックせずに実行できる場合は報告する(その後発行する)
ポーリングは、非同期 API を実装するために使用できる非ブロッキング同期 API を提供します。従来の Unix およびWindowsで使用できます。主な問題は、発行プロセスが他に何もすることがないときに繰り返しポーリングすることで CPU 時間を浪費し、他のプロセスに使用できる時間が減ってしまうことです。また、ポーリング アプリケーションは基本的にシングル スレッドであるため、ハードウェアが実行できる I/O 並列処理を十分に活用できない可能性があります。
選択(/ポーリング)ループ
BSD Unixおよび、 BSD 実装を利用するか、BSD 実装をモデルにしたTCP/IPプロトコル スタックを備えたほぼすべてのシステムで使用できます。ポーリングのバリエーションである選択ループは、selectシステム コールを使用して、ファイル記述子で条件が発生するまで(たとえば、データが読み取り可能になったとき)、タイムアウトが発生するまで、またはシグナルが受信されるまで (たとえば、子プロセスが終了したとき) スリープします。呼び出しの戻りパラメータを調べることで、ループはどのファイル記述子が変更されたかを検出し、適切なコードを実行します。多くの場合、使いやすさを考慮して、選択ループはイベント ループselectとして実装され、コールバック関数が使用されることがあります。この状況は、イベント駆動型プログラミングに特に適しています。
この方法は信頼性が高く、比較的効率的ですが、「すべてがファイル」というUnixパラダイムに大きく依存しています。ファイル記述子を含まないブロッキング I/O は、プロセスをブロックします。select ループは、すべての I/O を中央の呼び出しに含めることができることも前提としています。独自の I/O を実行するライブラリは、この点で特に問題があります。さらに潜在的な問題として、select と I/O 操作が十分に分離されているため、select の結果が事実上嘘になることがあります。2 つのプロセスが 1 つのファイル記述子から読み取りを行っている場合 (おそらく悪い設計)、select は読み取りデータが利用可能であることを示し、読み取りが発行されるまでにそのデータが消えてしまい、ブロックが発生します。2 つのプロセスが 1 つのファイル記述子に書き込んでいる場合 (それほど珍しいことではありません)、select はすぐに書き込み可能であることを示しますが、その間に他のプロセスによってバッファーがいっぱいになったり、書き込みが使用可能なバッファーに対して大きすぎたり、その他の理由で受信者に適さなかったりするため、書き込みがブロックされる可能性があります。
select
選択ループは、例えば完了キュー方式で可能な究極のシステム効率には達しません。selectこれは、受け入れ可能なイベント セットの呼び出しごとの調整を可能にする呼び出しのセマンティクスが、選択配列をトラバースする呼び出しごとにある程度の時間を消費するためです。これにより、ウィンドウ システム用に 1 つのファイル記述子を開き、開いているファイル用にいくつかのファイル記述子を開くユーザー アプリケーションにはほとんどオーバーヘッドが生じませんが、潜在的なイベント ソースの数が増えるにつれて問題が大きくなり、C10k 問題epollのように、多数のクライアント サーバー アプリケーションの開発を妨げる可能性があります。このような場合には、他の非同期方式の方が著しく効率的である可能性があります。一部の Unixでは、より優れたスケーリングを備えたシステム固有の呼び出しが提供されています。たとえば、Linux (イベントが発生したイベント ソースのみで戻り選択配列を埋める)、FreeBSD、およびSolarisのイベント ポート (および )kqueueなどです。
/dev/poll
SVR3 Unix はシステム コールを提供しましたpoll。 よりも適切な名前であると言えますがselect、この説明の目的上、本質的には同じものです。SVR4 Unix (およびPOSIX ) は両方の呼び出しを提供します。
信号(割り込み)
BSDおよびPOSIX Unixで使用できます。I/O は非同期で発行され、完了するとシグナル(割り込み) が生成されます。低レベルのカーネル プログラミングと同様に、シグナル ハンドラー内で安全に使用できる機能は制限されており、プロセスのメイン フローはほぼどの時点でも中断される可能性があり、シグナル ハンドラーから見たデータ構造に一貫性がなくなります。シグナル ハンドラーは通常、単独ではそれ以上の非同期 I/O を発行できません。
シグナルアプローチは、OS 内での実装が比較的簡単ですが、アプリケーション プログラムに、オペレーティング システムのカーネル割り込みシステムの作成に関連する不要な負担をもたらします。その最悪の特徴は、すべてのブロッキング (同期) システム コールが潜在的に割り込み可能であることです。プログラマーは通常、各コールで再試行コードを組み込む必要があります。[引用が必要]
コールバック関数
従来の Mac OS、VMS、Windowsで使用できます。シグナル メソッドと基本的に同じであるため、シグナルメソッドの多くの特性を備えていますが、そのように認識されることはほとんどありません。違いは、各 I/O 要求には通常、独自の完了関数があるのに対し、シグナルシステムには単一のコールバックがあることです。
一方、コールバックを使用する場合の潜在的な問題は、スタックの深さが管理できないほど大きくなる可能性があることです。これは、1 つの I/O が終了したときに、別の I/O をスケジュールすることが非常に一般的なためです。この要件をすぐに満たす必要がある場合、最初のコールバックは、次のコールバックが呼び出される前にスタックから「巻き戻される」ことはありません。これを防ぐシステム (新しい作業の「中間」スケジュールなど) は、複雑さを増し、パフォーマンスを低下させます。ただし、実際には、新しい I/O 自体は通常、開始されるとすぐに戻り、スタックが「巻き戻される」ため、これは通常問題になりません。この問題は、キューを使用して、最初のコールバックが戻るまで、それ以上のコールバックを回避することによっても防ぐことができます。
軽量プロセスまたはスレッド
軽量プロセス(LWP) またはスレッドは、ほとんどの最新のオペレーティング システムで使用できます。プロセスメソッドに似ていますが、オーバーヘッドが低く、フローの調整を妨げるデータ分離がありません。各 LWP またはスレッド自体は、プログラミング ロジックを簡素化する従来のブロッキング同期 I/O を使用します。これは、Java や Rust を含む多くのプログラミング言語で使用される一般的なパラダイムです。マルチスレッドでは、カーネルが提供する同期メカニズムとスレッドセーフライブラリを使用する必要があります。この方法は、必要なスレッドの数が多いため、Web サーバーなどの非常に大規模なアプリケーションには適していません。
このアプローチは、 Erlangプログラミング言語のランタイム システムでも使用されています。Erlang仮想マシンは、数個のスレッドまたは場合によっては 1 つのプロセスのみの小さなプールを使用して非同期 I/O を使用し、最大数百万の Erlang プロセスからの I/O を処理します。各プロセスでの I/O 処理は、主にブロッキング同期 I/O を使用して記述されます。このようにして、非同期 I/O の高パフォーマンスと通常の I/O のシンプルさが融合されます (アクター モデルを参照)。Erlang の多くの I/O 問題はメッセージ パッシングにマッピングされており、組み込みの選択的受信を使用して簡単に処理できます。
ファイバー/コルーチンは、 Erlang プロセスとまったく同じ保証を提供するわけではありませんが、Erlang ランタイム システムの外部で非同期 I/O を実行するための同様に軽量なアプローチとして考えることができます。
完了キュー/ポート
Microsoft Windows、Solaris、AmigaOS、DNIX、Linux(5.1以降で利用可能なio_uringを使用)で利用可能。 [1] I/O要求は非同期に発行されますが、完了の通知は完了順に同期キューメカニズムを介して提供されます。通常、メインプロセスの状態マシン構造(イベント駆動型プログラミング)に関連付けられており、非同期I/Oを使用しないプロセスや他の形式を使用するプロセスとはほとんど似ていないため、コードの再利用が妨げられます[要出典] 。追加の特別な同期メカニズムやスレッドセーフなライブラリを必要とせず、テキスト(コード)と時間(イベント)のフローが分離されていません。
イベントフラグ
VMSおよびAmigaOSで使用可能(多くの場合、完了ポートと組み合わせて使用されます)。本質的には深さ 1 の完了キューであるため、完了キュー方式の多くの特性を備えています。キューの「深さ」の影響をシミュレートするには、潜在的な未処理 (ただし完了) イベントごとに追加のイベント フラグが必要です。そうしないと、イベント情報が失われる可能性があります。このような塊で次の利用可能なイベントを待機するには、同期メカニズムが必要ですが、これは、潜在的に並列なイベントの数が多い場合に適切にスケーリングされない可能性があります。
チャネルI/O
IBM、Groupe Bull、およびUnisysのメインフレームで利用できます。チャネル I/O は、ほとんどの I/O をコプロセッサにオフロードすることで、CPU の使用率とスループットを最大化するように設計されています。コプロセッサにはオンボード DMA があり、デバイスの割り込みを処理し、メイン CPU によって制御され、本当に必要な場合にのみメイン CPU に割り込みます。このアーキテクチャは、チャネル プロセッサ上で実行され、I/O アクティビティとプロトコルの重い処理を実行する、いわゆるチャネル プログラムもサポートします。
登録I/O
Windows Server 2012およびWindows 8で利用可能。多数の小さなメッセージを処理するアプリケーション向けに最適化されており、ジッターと遅延を減らして1 秒あたりの I/O 操作数を増やします。 [2]
実装
汎用コンピューティング ハードウェアの大部分は、非同期 I/O を実装する 2 つの方法、つまりポーリングと割り込みに完全に依存しています。通常、両方の方法が一緒に使用されますが、そのバランスはハードウェアの設計と必要なパフォーマンス特性に大きく依存します。( DMA自体は別の独立した方法ではなく、ポーリングまたは割り込みごとにより多くの作業を実行できる手段にすぎません。)
純粋なポーリング システムも完全に可能であり、小型のマイクロコントローラ ( PIC を使用するシステムなど) は、この方法で構築されることがよくあります。CP /Mシステムも、DMA の有無にかかわらず、この方法で構築できます (ただし、まれにしか行われませんでした)。また、他の潜在的なタスクを犠牲にして、少数のタスクにのみ最大限のパフォーマンスが必要な場合、割り込みのオーバーヘッドが望ましくない場合があるため、ポーリングも適切である可能性があります。(割り込みを処理するには、プロセッサ状態の少なくとも一部を保存するための時間 [と領域] と、中断されたタスクを再開するために必要な時間が必要です。)
汎用コンピューティング システムのほとんどは、割り込みに大きく依存しています。純粋な割り込み システムも可能かもしれませんが、通常はポーリングのコンポーネントも必要です。これは、複数の潜在的な割り込み ソースが共通の割り込み信号ラインを共有することが非常に一般的であるためです。この場合、デバイス ドライバー内でポーリングを使用して実際のソースを解決します。(この解決時間も、割り込み システムのパフォーマンス低下の一因となります。割り込みの処理に関連するオーバーヘッドを最小限に抑えるために、長年にわたり多大な作業が行われてきました。現在の割り込み システムは、高度に調整された以前のシステムと比較するとかなり無精ですが、ハードウェア パフォーマンスの全体的な向上により、この問題は大幅に軽減されています。)
ハイブリッド アプローチも可能で、割り込みによって非同期 I/O のバーストの開始をトリガーし、そのバースト自体の中でポーリングが使用されます。この手法は、ネットワークやディスクなどの高速デバイス ドライバーでよく使用されます。これらのデバイスでは、割り込み前のタスクに戻るまでに失われる時間が、次に必要なサービスまでの時間よりも長くなります。(現在使用されている一般的な I/O ハードウェアは、比較的パフォーマンスの低い割り込みシステムを補うために、DMA と大容量データ バッファーに大きく依存しています。これらは、ドライバー ループ内でポーリングを使用するのが特徴で、非常に高いスループットを発揮できます。理想的には、データごとのポーリングは常に成功するか、最大でも数回繰り返される程度です。)
かつては、このようなハイブリッド アプローチは、DMA や十分なバッファリングが利用できないディスク ドライバやネットワーク ドライバで一般的でした。必要な転送速度は、最小 4 操作のデータごとのループ (ビット テスト、条件付き分岐、フェッチ、およびストア) を許容できる速度よりも速かったため、ハードウェアは I/O デバイスで自動待機状態生成を行うように構築されることがよくありました。これにより、データ準備完了のポーリングがソフトウェアからプロセッサのフェッチまたはストア ハードウェアにプッシュされ、プログラムされたループが 2 操作に削減されました (実質的にプロセッサ自体を DMA エンジンとして使用します)。6502プロセッサは、アサートされるとプロセッサのオーバーフロー ビットが直接設定されるハードウェア ピンを備えていたため、3 要素のデータごとのループを提供する珍しい手段を提供しました (当然、デバイス ドライバの外部でオーバーフロー ビットが上書きされないようにハードウェア設計に細心の注意を払う必要があります)。
合成
これら 2 つのツール (ポーリングと割り込み) のみを使用して、上で説明した他のすべての形式の非同期 I/O を合成できます (実際に合成されています)。
Java 仮想マシン(JVM)などの環境では、JVM が稼働している環境で非同期 I/O がまったく提供されていない場合でも、非同期 I/O を合成できます。これは、JVM の解釈型の性質によるものです。JVM は、定期的にポーリング (または割り込み) して、内部の制御フロー変更を開始し、複数の同時プロセスが存在するように見せかけます。これらのプロセスの少なくとも一部は、非同期 I/O を実行するために存在していると考えられます。(もちろん、ミクロレベルでは並列処理がかなり粗く、理想的でない特性を示す場合もありますが、表面的には希望どおりのように見えます。)
実際、これは、何らかの形でポーリングを使用して異なる形式の非同期 I/O を合成する場合の問題です。ポーリングであるすべての CPU サイクルは無駄になり、目的のタスクを達成するのではなく、オーバーヘッドによって失われます。ポーリングではないすべての CPU サイクルは、保留中の I/O に対する反応の待ち時間の増加を表します。これら 2 つの相反する力の間で許容できるバランスをとることは困難です。(これが、ハードウェア割り込みシステムが最初に発明された理由です。)
効率を最大化する秘訣は、割り込みを受信したときに適切なアプリケーションを起動するために必要な作業量を最小限に抑えることです。次に重要なのは (ただし、それほど重要ではないかもしれませんが)、アプリケーション自体が実行する必要があることを決定する方法です。
特に問題となるのは (アプリケーションの効率性に関して) 公開されているポーリング メソッド (select/poll メカニズムを含む) です。関心のある基礎となる I/O イベントは、おそらく割り込み駆動型ですが、このメカニズムとのやり取りはポーリングされ、ポーリングに長い時間がかかることがあります。これは、select (および poll) によって可能になる可能性のある大規模なポーリングに特に当てはまります。割り込みは、シグナル、コールバック関数、完了キュー、およびイベント フラグに非常によくマップされるため、このようなシステムは非常に効率的です。
例
次の例は、I/O を読み取るための 3 つのアプローチを示しています。オブジェクトと関数は抽象的です。
1. ブロッキング、同期:
device = IO.open () data = device.read ( ) #デバイスにデータがあるまでスレッドはブロックされますprint ( data )
2. ブロッキングと非ブロッキング、同期: (ここではIO.poll()最大 5 秒間ブロックしますが、device.read()ブロックしません)
device = IO.open ( ) ready = False while not ready : print ( "読み取るデータがありません!" ) ready = IO.poll ( device , IO.INPUT , 5 ) # 5秒が経過するか、読み取るデータ( INPUT )がある場合に制御を返しますdata = device.read ( ) print ( data )
3. 非ブロッキング、非同期:
ios = IO.IOService ( )デバイス= IO.open ( ios )
def inputHandler ( data , err ):
"入力データ ハンドラー"
if not err :
print ( data )
device.readSome ( inputHandler ) ios.loop () #すべての操作が完了するまで待機し、適切なハンドラをすべて呼び出します
以下はAsync/awaitを使用した同じ例です。
ios = IO.IOService ( )デバイス= IO.open ( ios )
async def task ():
try :
data = await device . readSome ()
print ( data )
except Exception :
pass
ios.addTask ( task ) ios.loop () # すべての操作が完了するまで待機し、適切なハンドラをすべて呼び出します
以下はReactor パターンの例です。
デバイス = IO.open ( )リアクター= IO.Reactor ( )
def inputHandler ( data ): "
入力データハンドラー"
print ( data )
reactor.stop ( )
reacter.addHandler ( inputHandler , device , IO.INPUT ) reacter.run ( ) # イベントを処理して適切なハンドラを呼び出す reacter を実行します
参照
参考文献
- ^ Corbet, Jonathan. 「新しい非同期I/O APIの導入」LWN.net 。 2020年7月27日閲覧。
- ^ 「登録済み入出力 (RIO) API 拡張機能」。technet.microsoft.com。2016年8 月 31 日。
外部リンク
- C10K 問題: スケーリングを重視した非同期 I/O 方式の調査 - Dan Kegel 著
- M. Tim Jones による記事「非同期 I/O を使用してアプリケーションのパフォーマンスを向上させる」
- Willy Zwaenepoel、Khaled Elmeleegy、Anupam Chanda、Alan L. Coxによる記事「イベント駆動型サーバー向けの遅延非同期 I/O」
- I/O操作を並列に実行する
- POSIX標準からの説明
- I/O 完了ポートの内部 ( Mark Russinovich著)
- .NET Framework 開発者ガイドからの説明
- 非同期 I/O と非同期ディスク I/O エクスプローラー
- IO::AIOは、ほとんどのI/O操作に非同期インターフェースを提供するPerlモジュールです。
- ACE プロアクター
