Async訪ねて3000里 (6) : I/O完了ポートによるワーカースレッドの運用

前回、TaskCompletionSourceによるタスクの継続に、APCコールバックを使用した場合の欠点を示しました。

APCコールバックはユーザーモードから見た場合の挙動がわかりやすい(コールバックが実行される)のですが、MsgWaitForMultipleObjectsEx APIなどのAPCコールバック対応APIで待機する必要があり、待機したスレッド(通常はメインスレッド)からコールバックが呼び出されるため、そのスレッドだけでI/Oの完了を捌く必要があります。

仮にシステムに多くのCPUコアが存在する場合でも、I/Oの完了による継続処理(翻ってawait後の処理)が、単一のスレッドだけで処理されることになり、パフォーマンスが上がりません。


手が遅いので指摘されてしまいました (;´Д`) 最後のピースを埋めましょう。

カーネルモードからユーザーモードに、I/Oの完了を通知する方法は、APCコールバック以外にも方法があります。

ファイルハンドルやイベントオブジェクトなどのWin32カーネルオブジェクトは、「シグナル状態」と呼ばれる一種のフラグを持っています。このフラグは、それぞれ決められたタイミングで「非シグナル状態」から「シグナル状態」と変化します。たとえば、ファイルハンドルであれば、直前のI/O操作が完了した(IoCompleteRequest)タイミングで、ファイルハンドルがシグナル状態となります。

APCコールバックを使用する場合は、I/Oが完了した事をプロセス毎のAPCキューに挿入することで通知しましたが、シグナル状態による通知であれば、キューを操作するコストを削減出来、さらにAPCコールバック対応のAPIで待機することなく、完了した事を通知することができます。但し、シグナル状態による通知は、前述のとおり一種のフラグであるため、コールバックとは異なり、この契機で能動的に処理を行うことができません。

このシグナル状態は、WaitForSingleObjectWaitForMultipleObject APIで監視することができます。つまり、これらのAPIを使用すると、I/O処理が完了するタイミングまで、スレッドを効率よく待機(ハードブロック)することができます。

WaitForSingleObjectでは単一のスレッドがシグナル状態となるまで待機しますが、これをワーカースレッドによる複数のスレッドにまで拡張したものが「I/O完了ポート」です。I/O完了ポートを使用すると、複数のワーカースレッドをあらかじめ待機させておき、シグナル状態となる通知によって、次々とワーカースレッドを使って完了後の処理を実行することができます。

実はasync/awaitの非同期インフラストラクチャーは、APCコールバックではなく、I/O完了ポートによって実装されています。I/O完了ポートに紐づけられるワーカースレッド群は、ThreadPoolクラスのI/Oスレッドとして内部的に割り当てられています。ThreadPoolクラスのワーカースレッドは、古くはIAsyncResultインターフェイスによる初期の非同期インフラの駆動用として使用されてきました。.NET 3.5以降でThreadPoolの実装が見直され、より効率よく動作するようになり、TaskAwaiterのコールバック駆動用として使用しています。

つまり、APCコールバックではAPC待機APIの呼び出しで待機したスレッドがコールバックを実行していましたが、現在の非同期インフラはThreadPoolクラスが保持するI/Oスレッド群が、TaskAwaiterのコールバックを実行しています。そして管理はI/O完了ポートで行っているため、大量のI/O要求の完了処理も、複数のスレッドで同時に処理できるのです。

MSDNブログに、.NETにおけるI/O完了ポートの使用方法が示されていたので、参考にして下さい。


以上をまとめると、async/awaitによる非同期処理は、ハードウェアからの完了通知としてもっともロスが少ないと考えられ、理想的な環境であれば、余計なワーカースレッドは使われません。但し、I/Oの完了がユーザーモードに通知された際に、TaskAwaiterのコールバック処理(await文以降の処理)を実行するためにスレッドが必要なため、この時に限り、効率的にプールされたワーカースレッドを使います。

唯一このタイミングでのみ、わずかな期間(IoCompleteRequestからI/O完了ポートでスレッドが処理に割り当てられるまで)ですが、スレッドのハードブロックと見なせる空白の時間が存在しますが、その他の手法と比べて無駄がありません。モニタAPIや待機APIによる、余計なハードブロックを起こさないのですから、多数のCPUコアで並列実行する場合でも、コンテキストスイッチによるロスは皆無です。

ケースバイケースですが、従来は自前ワーカースレッドによる複雑な実装しか選択肢が無かったものが、async/awaitの構文だけで効率の高い処理を簡便に実現できるようになったのです。ぜひ、非同期処理を実践して行きましょう。

せがゆうさん、ありがとうございました!