平々毎々(アーカイブ)

はてなダイアリーのアーカイブです。

Windows 8と2012 ServerのRegistered I/Oについての記事

IOCPとRIOのイメージを書いてみた。技術的にはあんまり正しくないと思うけど。

RIOは最初こう書いてみたものの

書き直してみた。どっちにしろ正しさは保証できない。

技術的にちゃんとしてる説明は、まずはMSDN

What's New for Windows Sockets
https://msdn.microsoft.com/en-us/library/windows/desktop/ms740642.aspx#Updated_for__Windows_8_and_Windows_Server_2012

でも英語。

それから、Channel9にBUILD 2011のときの動画があるからそれを観ろと。

New techniques to develop low-latency network apps
https://channel9.msdn.com/events/Build/BUILD2011/SAC-593T

でも英語だし、時間かかるしなあ。

非公式だけどThe Server FrameworkというC++の有償フレームワークがあって、そこのブログがいろいろ解説記事を書いてる。

Winsock Registered I/O Archives
http://www.serverframework.com/asynchronousevents/rio/

やぱりこれも英語。
読み始めるならこのあたりからか。

Windows 8 Registered I/O Networking Extensions
http://www.serverframework.com/asynchronousevents/2011/10/windows-8-registered-io-networking-extensions.html
Windows 8 Registered I/O and I/O Completion Ports
http://www.serverframework.com/asynchronousevents/2011/10/windows-8-registered-io-and-io-completion-ports.html

後者の方について適当翻訳をしてみた。気が向いたら他の記事も見てみる。





Windows 8のRegistered I/OとI/O完了ポート

前回のブログ記事でWindows 8RIOネットワーク拡張を紹介した。RIOで処理の完了を受け取るには3つの方法がある。ポーリング、イベントドリブン、そしてIOCPを使う方法。RIOは柔軟だからいろんな形で使える。ポーリングは高速なUDPとかHFTとかでCPUを使い切れてうれしい。イベントドリブンはスピンロックしなくていいから効率的だけど、今一番興味があるのはIOCPスタイル。普通のネットワーク処理ではこれが一番パフォーマンスがいいんじゃないかな。たぶんだけど。

ちなみにこれらの記事は俺の前提とか直感とかに基づいてるし、SDKとかと同期してるわけじゃないよ。まあ話半分で聞いてくれ。

で、RIOとIOCPを合わせて使うには。

RIOの場合は処理の完了はキューに入る。固定長のデータ構造で、ユーザー空間とカーネル空間で共用されるからユーザーモードでデキューできる(詳しくはBUILDのビデオ観れ)。前回も書いたけど、完了をどうやって受け取るかはキューを作るときに指定する。イベントを渡すか、IOCPを渡すか、何も渡さない場合は自分でポーリング。IOCPなら、あらかじめRIONotify()を呼んでおくと、GetQueuedCompletionStatus()でキューが空だったらI/Oスレッドがプールに戻されて、キューが空じゃなくなると通知が来て処理が続く。サンプルコードはこんな感じ。マジで使うならcompletionKeyとかいろいろやることあるかんね。

if (::GetQueuedCompletionStatus(
   hIOCP, 
   &numberOfBytes,
   &completionKey,
   &pOverlapped,
   INFINITE))
{
   const DWORD numResults = 10;
 
   RIORESULT results[numResults];
 
   ULONG numCompletions = rio.RIODequeueCompletion(
      queue,
      results,
      numResults);
 
   while (numCompletions)
   {
      for (ULONG i = 0; i < numCompletions; ++i)
      {
         // deal with request completion...
      }
 
      numCompletions = rio.RIODequeueCompletion(
         queue,
         results,
         numResults);
   }
 
   rio.RIONotify(queue);
}

で、空じゃなくなったら通知されると言ったが、RIONotify()につき通知は1回きり。RIODequeueCompletion()でデキューした後で、次のIOCP完了を受け取りたければふたたびRIONotify()を呼ぶんだ、いいね?面倒なようだけどもこれはこれでいいんだよ。完了キューにアクセスするスレッドを自分で制御できる(訳者メモ:たぶん、RIONotifyを呼び出したスレッドと、引数として渡したキューとが紐づけられるんだろうと思った)から競合とか考えなくていいんだから。送信と受信で1個ずつキューを使って、それぞれ別スレッドにしてもいい。送受信でキューを1つしか使わなければ、あるソケットに紐づくのは同時には1スレッドだけってことになる。普通のIOCPはスレッドプールを使うから、そこが違う(訳者メモ:ひとつのI/Oが完了すると必ずひとつのスレッドが割り当てられるから、複数のI/Oが絡むと必ずマルチスレッドになる)。

RIOのメリット

順序制御が楽、マジで楽。完了キューには期待した通りの順序で入ってくるから、1スレッドでアクセスすれば期待した順序で取り出せるでしょ?IOCPでも通知は順番通りだけど、スレッドプールのせいで順序制御に余計な手間がかかることを考えれば、RIOの方が簡単。あとRIOには送受信バッファがない。(訳者メモ:そのあとも何かちょっと書いてあるけど読み飛ばし)
他のメリットは1スレッドで続けてデキューできること。デキューしてる間はカーネルモードに遷移する必要もないし、キューが空になってからカーネルモードに入る的なやつで、いい感じ。

RIOのデメリット

I/Oスレッドをフル活用するのは大変だよーん。キューの数とかスレッドの数とかは自分で制御することになるわけだし。一般的な用途ではRIOを生で触るのは辛いから、そういうときは、新しいコネクションをどのキューに割り振るかとか、キューの増減どうするかとか、そういうのをやってくれるフレームワーク的なものが欲しい。キューの数が増えてるにもかかわらずI/Oスレッドが足りない場合、1スレッドの処理を1つのキューにかかりきりにすると、スレッドが割り当たってないキューのコネクションはいつまでも待たされてしまうけどそれでいいの?って話もある。
それから、I/OスレッドがRIONotify()を呼ぶ前にうっかり別の処理でブロックしてしまったりすると、そのキューで扱うソケットは全部待ちになってしまうから、他の接続に迷惑をかけることになるよ。

まとめ

RIOとIOCPを合わせて使うのは簡単に見えるけど詳細はただのIOCPとは結構違うから注意。RIOから見ればIOCPのとこがブロック処理になるけど、キューと対応付けた複数のコネクションを1スレッドで扱える。