非同期I/O

Form of input/output processing

コンピュータサイエンスにおいて、非同期I/O非シーケンシャルI/Oとも呼ばれる)は、入出力(I/O)処理の一種であり、I/O操作が完了する前に他の処理を続行することを可能にします。Windows APIでは、非同期I/OはオーバーラップI/Oと呼ばれます。

コンピュータにおける入出力操作は、データ処理に比べて非常に遅くなることがあります。I/Oデバイスには、読み取りまたは書き込みを行うトラックをシークするハードドライブなど、物理的に移動する機械装置が組み込まれている場合があります。この動作は、電流のスイッチングよりも桁違いに遅いことがよくあります。例えば、10ミリ秒かかるディスク操作中に、 1GHzのクロック周波数を持つプロセッサは、1000万回の命令処理サイクルを実行できます。

I/Oに対するシンプルなアプローチは、アクセスを開始し、完了するまで待つことです。しかし、同期I/OまたはブロッキングI/Oと呼ばれるこのアプローチは、通信が進行中にプログラムの進行をブロックし、システムリソースをアイドル状態にします。プログラムが多くのI/O操作を実行する場合(例えば、ユーザー入力に大きく依存するプログラムなど)、プロセッサはI/O操作の完了を待つために、ほぼすべての時間をアイドル状態に費やす可能性があります。

あるいは、通信を開始してから、I/Oの完了を必要としない処理を実行することも可能です。このアプローチは非同期入出力と呼ばれます。I/Oの完了に依存するタスク(読み取り値の使用と、書き込み操作の完了を保証する重要な操作の両方が含まれます)は、I/O操作の完了を待機する必要があるため、依然としてブロックされますが、I/O操作に依存しない他の処理は続行できます。

非同期I/Oを様々なレベルで実装するためのオペレーティングシステム関数が数多く存在します。実際、最も基本的なオペレーティングシステムを除けば、ほとんどのオペレーティングシステムの主要な機能の一つは、少なくとも何らかの基本的な非同期I/Oを実行することです。ただし、これはユーザーやプログラマーにとって特に明白ではないかもしれません。最も単純なソフトウェアソリューションでは、ハードウェアデバイスの状態を一定間隔でポーリングし、デバイスが次の操作の準備ができているかどうかを検出します。(例えば、CP/Mオペレーティングシステムはこのように構築されました。そのシステムコールセマンティクスは、これよりも複雑なI/O構造を必要としませんでしたが、ほとんどの実装はより複雑で、それによってより効率的でした。)直接メモリアクセス(DMA)はポーリングベースのシステムの効率を大幅に向上させ、ハードウェア割り込みはポーリングの必要性を完全に排除します。マルチタスクオペレーティングシステムは、ハードウェア割り込みによって提供される機能を活用しながら、割り込み処理の複雑さをユーザーから隠蔽することができます。スプーリングは、非同期I/Oを活用するために設計された最初のマルチタスク形式の1つでした。最後に、ユーザー プロセス内のマルチスレッドと明示的な非同期 I/O API を使用すると、ソフトウェアの複雑さが増すものの、非同期 I/O をさらに活用できます。

非同期I/Oはエネルギー効率、そして場合によってはスループットを向上させるために使用されます。ただし、場合によってはレイテンシとスループットに悪影響を与える可能性があります。

フォーム

I/O の形式と POSIX 関数の例:

ブロッキング 非ブロッキング 非同期
API 書く、読む 書き込み、読み取り + 投票 / 選択 aio_write、aio_read

あらゆる形式の非同期I/Oは、アプリケーションに潜在的なリソース競合とそれに伴う障害を引き起こす可能性があります。これを防ぐには、慎重なプログラミング(多くの場合、相互排他制御セマフォなどを使用)が必要です。

アプリケーションに非同期I/Oを公開する場合、実装にはいくつかの大まかなクラスがあります。アプリケーションに提供されるAPIの形式は、オペレーティングシステムが実際に提供するメカニズムと必ずしも一致するわけではなく、エミュレーションが可能です。さらに、アプリケーションのニーズやプログラマの要望に応じて、1つのアプリケーションで複数のメソッドが使用される場合もあります。多くのオペレーティングシステムはこれらのメカニズムを複数提供しており、すべてのメカニズムを提供するオペレーティングシステムもある可能性があります。

プロセス

初期のUnixで利用可能でした。マルチタスクオペレーティングシステムでは、処理は複数のプロセスに分散されます。各プロセスは独立して動作し、独自のメモリを持ち、独自のI/Oフローを処理します。これらのフローは通常、パイプラインで接続されます。プロセスの作成と維持にはかなりのコストがかかるため[要出典]、このソリューションはプロセスセットが小規模で比較的安定している場合にのみ有効です。また、個々のプロセスは互いのI/O処理を除いて独立して動作できることを前提としています。もしプロセスが他の方法で通信する必要がある場合、それらの連携は困難になる可能性があります。[要出典]

このアプローチの拡張はデータフロー プログラミングであり、これにより、パイプがサポートするチェーンだけでなく、より複雑なネットワークが可能になります。

投票

バリエーション:

  • まだ実行できない場合はエラー(後で再発行)
  • ブロックせずに実行できる場合は報告する(その後発行する)

ポーリングは、非同期APIの実装に使用できる非ブロッキング同期APIを提供します。従来のUnixおよびWindowsで利用可能です。ポーリングの主な問題は、発行プロセスが他に何もすることがないにもかかわらず、繰り返しポーリングを行うことでCPU時間を浪費し、他のプロセスに利用可能な時間を減少させることです。また、ポーリングアプリケーションは基本的にシングルスレッドであるため、ハードウェアが実現可能なI/O並列処理を十分に活用できない可能性があります。

選択(/ポーリング)ループ

BSD Unix 、および BSD 実装を利用またはモデル化したTCP/IP プロトコル スタックを備えたほぼすべてのシステムで使用できます。ポーリングのバリエーションである選択ループは、selectシステム コールを使用して、ファイル記述子で条件が発生するまで(たとえば、データが読み取り可能になったとき)、タイムアウトが発生するまで、またはシグナルが受信されるまで (たとえば、子プロセスが終了したとき) スリープします。呼び出しの戻りパラメータを調べることによりループはどのファイル記述子が変更されたかを検出し、適切なコードを実行します。多くの場合、使いやすさを考慮して、選択ループはイベント ループとして実装され、コールバック関数が使用されることがあります。この状況は、イベント駆動型プログラミングに特に適していますselect

この方法は信頼性が高く、比較的効率的ですが、すべてがファイルである」というUnixパラダイムに大きく依存しています。ファイル記述子を含まないブロッキングI/Oはプロセスをブロックします。selectループも、すべてのI/Oを中央の呼び出しに含めることができることを前提としています。独自のI/O処理を実行するライブラリは、この点で特に問題となります。さらに潜在的な問題として、selectとI/O操作が十分に分離されているため、selectの結果が事実上偽となる可能性があることが挙げられます。2つのプロセスが単一のファイル記述子から読み取りを行う場合(設計上は問題があると言えるでしょう)、selectは読み取りデータが利用可能であると示しますが、読み取りが発行されるまでにそのデータが消失し、ブロックが発生します。2つのプロセスが単一のファイル記述子に書き込みを行う場合(それほど珍しいことではありません)、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内での実装は比較的容易ですが、アプリケーションプログラムにとっては、OSのカーネル割り込みシステムの開発に伴う厄介な負担となります。その最大の欠点は、すべてのブロッキング(同期)システムコール潜在的に割り込み可能であることです。そのため、プログラマは通常、各呼び出しごとに再試行コードを組み込む必要があります。[要出典]

コールバック関数

クラシックなMac OSVMSWindowsで利用可能です。シグナルメソッドと基本的に同じ機能であるため、シグナルメソッドの多くの特徴を備えていますが、シグナルメソッドと認識されることはほとんどありません。違いは、各I/O要求は通常、独自の完了関数を持つことができるのに対し、シグナルシステムには単一のコールバックしかないことです。

一方、コールバックを使用する場合の潜在的な問題は、スタックの深さが管理不能なほどに増大する可能性があることです。これは、ある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 WindowsSolarisAmigaOSDNIXLinuxio_uringを使用、5.1 以上で利用可能)で利用可能。 [1] I/O 要求は非同期に発行されますが、完了の通知は完了順に同期キュー メカニズムを介して提供されます。通常、メイン プロセスの状態マシン構造(イベント駆動型プログラミング)に関連付けられており、非同期 I/O を使用しないプロセスや他の形式を使用するプロセスとはほとんど類似していないため、コードの再利用が妨げられます[要出典] 。追加の特別な同期メカニズムやスレッドセーフなライブラリを必要とせず、テキスト(コード)と時間(イベント)のフローが分離されていません。

イベントフラグ

VMSおよびAmigaOSで利用可能(多くの場合、補完ポートと組み合わせて使用​​されます)。本質的には深さ1の補完キューであるため、補完キュー方式の多くの特性を備えています。キューの「深さ」の影響をシミュレートするには、未処理(ただし完了)の可能性のあるイベントごとに追加のイベントフラグが必要です。そうしないと、イベント情報が失われる可能性があります。このようなイベントの塊の中で次の利用可能なイベントを待機するには、同期メカニズムが必要ですが、潜在的に並列なイベントの数が多い場合、適切にスケーリングできない可能性があります。

チャネルI/O

IBMGroupe BullUnisysのメインフレームで利用可能ですチャネル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への反応遅延の増加につながります。これら相反する二つの力の間で、適切なバランスを取ることは困難です。(そもそもハードウェア割り込みシステムが発明されたのは、まさにこのためです。)

効率を最大化する秘訣は、割り込み受信時に適切なアプリケーションを起動するために必要な作業量を最小限に抑えることです。次に(しかし、おそらく同様に重要なのは)、アプリケーション自体が実行すべき処理を決定する方法も重要です。

特に(アプリケーションの効率性という点で)問題となるのは、select/pollメカニズムを含む、公開されているポーリングメソッドです。対象となる基盤となるI/Oイベントは、おそらく割り込み駆動型ですが、このメカニズムとのやり取りはポーリングされ、ポーリングに多大な時間を消費する可能性があります。これは、select(およびpoll)によって可能になる可能性のある大規模なポーリングにおいて特に顕著です。割り込みはシグナル、コールバック関数、完了キュー、イベントフラグに非常によく対応しており、このようなシステムは非常に効率的です。

以下の例は、I/Oを読み取るための3つのアプローチを示しています。オブジェクトと関数は抽象的です。

1. ブロッキング、同期:

device :  Device  =  IO.open ( ) data : Data = device.read ( ) # デバイスにデータが存在するまでスレッドはブロックさますprint ( data )
     

2. ブロッキングと非ブロッキング、同期: (ここではIO.poll()最大 5 秒間ブロックされますがdevice.read()

device :  Device  =  IO.open () ready : bool = False while not ready : print ( "読み取るデータありません!" ) ready = IO.poll(device, IO.INPUT, 5 ) # 5経過読み取るデータ( INPUT )がある場合に制御を返しますdata : Data = device.read ( ) print ( data )
   
  
    
          
   

3. 非ブロッキング、非同期:

ios :  IOService  =  IO.IOService ( ) device : Device = IO.open ( ios )
   

def input_handler ( data : Data , err : Error ) -> None : """入力データハンドラー""" if not err : print ( data )      
    
      
        

device.read_some ( input_handler ) ios.loop ( ) # すべて操作が完了するまで待機し、適切なハンドラーをすべて呼び出します
  

以下はasync/awaitを使用した同じ例です

ios :  IOService  =  IO.IOService ( ) device : Device = IO.open ( ios )
   

async  def task () -> None : try : data : Data = await device . read_some () print ( data ) except Exception : pass   
     
            
        
     
        

ios.addTask ( task ) ios.loop ( ) # すべての操作完了するまで待機し、適切なハンドラをすべて呼び出します
  

以下はReactor パターンの例です

デバイス: デバイス =  IO.open ( )リアクター:リアクター= IO.Reactor ( )
   

def input_handler ( data : Data ) -> None : """入力データハンドラー""" print ( data ) reactor . stop ()    
    
    
    

reacters.add_handler ( input_handler , device , IO.INPUT ) reacters.run () #イベント処理し適切なハンドラを呼び出す reacters を実行ます  
  

参照

参考文献

  1. ^ Corbet, Jonathan. 「新しい非同期I/O APIの導入」LWN.net . 2020年7月27日閲覧
  2. ^ 「Registered Input-Output (RIO) API Extensions」. 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 プロアクター
Retrieved from "https://en.wikipedia.org/w/index.php?title=Asynchronous_I/O&oldid=1325416735"