イベントループ

イベントループ (event loop)、メッセージディスパッチャ (message dispatcher)、メッセージループ (message loop)、メッセージポンプ (message pump)、ランループ (run loop) とは、プログラム内でイベントやメッセージを待ち受け、それらをディスパッチ（配送）する構成要素である。内部または外部の「イベントプロバイダー」（通常、イベントが到着するまで要求をブロックする）に要求することで動作し、次いで適当なイベントハンドラー (event handler) を呼び出す（イベントのディスパッチ）。イベントプロバイダーが後述のファイルインタフェースに従う場合、イベントループは reactor と連携する形で使われることがあり、select() または poll() を使ってファイルインタフェースにアクセスする。イベントループはほぼ常にメッセージの発信元とは非同期に動作する。

イベントループはプログラムの中心的制御構造となっていることが多い。そのためそれをメインループ (main loop) またはメインイベントループ (main event loop) とも呼ぶ。そのようなプログラムではイベントループが最上位の制御構造となっており、そのため「メイン」と名づけられている。

 メッセージパッシング 
メッセージポンプという呼称は、プログラムのメッセージキュー（通常OSが割り当て、OSが所有する）からメッセージを汲み上げ、そのプログラム内で処理することに由来する。厳密には、イベントループはプロセス間通信の実装法の1つである。実のところメッセージ処理は多くのシステムに存在し、例えばMachのカーネルレベルのコンポーネントにもある。イベントループはメッセージを使用するシステムの実装技法の1つである。

 代替手法 
この手法は以下のような他の手法とは対照的である:
 古くからある、単純に一回動作して終了するプログラム。この種のプログラムは情報処理の最初期からあり、ユーザーとの対話手段を持たない。現在も主にCUI指向のプログラムでよく使われている。各種パラメータを指定して起動される。
 メニュー駆動型設計。この場合も一種のメインループは存在するが、ユーザーから見てイベント駆動的ではない。イベント駆動の代わりとして、階層型のメニューを順次選択していって、希望する動作を指定する。このメニューを通した限定的な対話性がある。

 例 
GUIが主流となったため、多くのアプリケーションがメインループを持つようになった。get_next_message() というルーチンは一般にOSが提供するもので、メッセージが到着するまでブロックされる。したがってこのループが動作するのは処理すべきものが存在する場合だけである。

 function main
     initialize()
     while message != quit
         message := get_next_message()
         process_message(message)
     end while
 end function

 ファイルインタフェース 
UNIXでは「あらゆるものはファイルである」というパラダイムにより、ファイルベースのイベントループが自然に生まれた。ファイルの読み書きだけでなく、プロセス間通信、ネットワーク通信、デバイス制御が全てファイルI/Oで行われ、対象はファイル記述子で指定される。selectおよびpollシステムコールを使えば、複数のファイル記述子の状態変化を同時に監視でき、読み込むべきデータが到着したことを検知できる。

例として、継続的に更新されるファイルから読み込んでその内容を X Window System に表示するPythonプログラムを示す。クライアントとはソケットを通じて通信する。
main(): 
    file_fd = open ("logfile")
    x_fd = open_display ()
    construct_interface ()
    while changed_fds = select ({file_fd, x_fd}):
        if file_fd in changed_fds:
            data = read_from (file_fd)
            append_to_display (data)
            send_repaint_message ()
        if x_fd in changed_fds:
            process_x_messages ()

 シグナル処理 
UNIXでファイルインタフェースに従わない数少ない例として、非同期イベント（シグナル）がある。シグナルはシグナルハンドラで受信する。シグナルハンドラは小さな制限されたコードであり、それが動作中はプログラム本体の処理はサスペンドされる。select()でブロック中にシグナルを受信して処理した場合、selectはEINTRというエラーコードを伴って早期に戻る。プログラムがCPUを使用している間にシグナルを受信すると、シグナルハンドラを実行する間は本体の実行がサスペンドされる。

したがってシグナルを考慮するには、シグナルハンドラで大域変数のフラグをセットし、イベントループのselect()呼び出しの直前と直後でそのフラグをチェックすればよい。フラグがセットされていたら、ファイル記述子でのイベントと同様にシグナルを処理する。しかしながら、この技法では競合状態が生じる。フラグのチェックとselect()呼び出しの間にシグナルが到着した場合、select()が他の理由で戻るまでシグナルを処理できない。

この問題を解決するため、POSIXではpselectシステムコールを提供している。これはselectに似ているがsigmaskという引数が追加されており、シグナルのマスクを設定できる。これを使えば、普段はシグナルをマスクしておき、selectを呼び出している間だけマスクを解除することができる。すると、シグナルは select がイベントを待ち受けている間だけ受信されることになる。ただし、pselect()が利用可能となったのは比較的最近のことで、たとえばLinuxの場合、Linuxカーネルのバージョン2.6.16より以前の版ではpselect()システムコールは実装されておらず、glibcでは競合状態の問題をはらんだ実装がなされていた。

より汎用的な代替技法として、非同期イベントをself-pipe trickと呼ばれる技法でファイルベースのイベントに変換してやる技法がある。これはシグナルハンドラでパイプに1バイトを書き込み、そのパイプのもう一方の端を主プログラムがselectで監視するという技法であるBUGS, 。Linuxカーネル 2.6.22では、signalfd()という新システムコールが追加された。これはシグナル受信用の特別なファイル記述子を生成する。

 実装例 
 Windowsアプリケーション 
Windowsにてユーザーとやりとりするプロセスを動作させる場合、イベントに応答するためのメッセージループが必須である。Windowsではイベントとメッセージは同等視されるWindowsにおける排他制御のためのカーネルオブジェクトのひとつとして「イベント」（Win32イベント）があるが、これはWin32メッセージとは関係ない。。イベントとしては、ユーザーとのやりとり、ネットワークのトラフィック、システム処理、タイマー、プロセス間通信などがある。対話型でないI/Oのみのイベントについては、がある。I/O完了ポートのループはメッセージループとは別に動作し、メッセージループと相互作用することがない。

大抵のWin32アプリケーションの「心臓部」はWinMain()WinMain function (winbase.h) | Microsoft Docs関数であり、ループ内でGetMessage()を呼び出す。GetMessage()はメッセージまたは「イベント」を受信するまでブロックする。何らかの選択的処理の後DispatchMessage()を呼び出し、対応するハンドラー用のコールバック関数 (ウィンドウプロシージャWindow Procedures - Windows applications | Microsoft Docs: WindowProc) にメッセージをディスパッチする。専用のウィンドウプロシージャのないメッセージはDefWindowProc()というデフォルトのハンドラーにディスパッチする。RegisterClass()でウィンドウクラスを登録する際にウィンドウプロシージャの関数ポインタを指定することができ、DispatchMessage()はメッセージの送信先ウィンドウハンドル (HWND) に対応するウィンドウプロシージャを呼び出す。

以下はMicrosoft Docs (旧MSDNライブラリ) に記載されている、メッセージループの実装例のひとつであるGetMessageW function (winuser.h) | Microsoft Docs:
MSG msg;
BOOL bRet;

while ((bRet = GetMessage(&msg, NULL, 0, 0)) != 0)
{
    if (bRet == -1)
    {
        // handle the error and possibly exit
    }
    else
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}
そのほか、ブロッキングせずにメッセージキューからメッセージを読み取るPeekMessage()PeekMessageW function (winuser.h) | Microsoft Docsを使う方法もある。

 メッセージの順序性 
のWindowsでは、システムや周辺機器が受信した順序でメッセージループにメッセージが到達することを保証している。これはマルチスレッドアプリケーションの設計で必須となる。

ただし、一部のメッセージには異なる規則が適用され、常に最後に受信されるメッセージや文書化された特別な優先順位で受信されるメッセージがあるGetMessage() function with message priority list.。

 フレームワーク 
MFC、Windows Forms、WPFといったアプリケーションフレームワークでは、ライブラリ側で既定のメッセージループの実装を持っているため、通常はアプリケーションコードで明示的に記述する必要はない。アプリケーションで必要となるイベントハンドラーのみを記述していく「イベント駆動型プログラミング」のスタイルを用いて効率的に開発できる。ただし、必要に応じてメッセージループを詳細にカスタマイズしたりIdle Loop Processing | Microsoft Docs、異なるフレームワークを相互運用したりするためのAPIも用意されているSharing Message Loops Between Win32 and WPF | Microsoft DocsWindows Forms and WPF interop input architecture | Microsoft Docs。

 Xlibのイベントループ 
Xlibを直接使用するXアプリケーションは、XNextEventファミリの関数を中心に構築される。XNextEventはイベントキューにイベントが到着するまでブロックし、イベントを受け取ると即座にアプリケーションが適切にそれを処理する。Xlibのイベントループが扱うのはウィンドウシステムのイベントのみである。他のファイルやデバイスについても待ち受ける必要がある場合は ConnectionNumber などのプリミティブからイベントループを自前で構築する必要があるが、一般にマルチスレッドを採用することが多い。

Xlibを直接使用するプログラムは少ない。より一般的にはXlib上に構築されたGUIツールキットを使用する。例えば、X Toolkit Intrinsics (Xt) 上のツールキットでは XtAppAddInput() と XtAppAddTimeout() を使用する。

なお、シグナル受信時の状態が不定であるため、シグナルハンドラからXlib関数を呼び出すのは危険であるX11R5、X11R6、Xtでの対処法は Chapter: 26 Signal Handling 。

 GLibのイベントループ 
GLibのイベントループはGTK+（現・GTK）向けに作られたが、今ではD-BusなどのGUI以外のアプリケーションでも使われている。ファイル記述子群で監視したいリソースを指定する。シグナルを受信するかタイムアウトすると、ブロック状態から復帰する。GLibはファイル記述子と子プロセス終了のイベントを組み込みでサポートしているが、prepare-check-dispatch モデルの対象として任意のイベントを加えることが可能であるThe Main Event Loop - Customizing the main loop iteration Gnome Dev Center。

GLibのイベントループを使っているアプリケーションライブラリとしては、GStreamerやGnomeVFSの非同期IOメソッド群があるが、最大のクライアントライブラリはGTKである。ウィンドウシステムからのイベント（Xの場合、Xのソケットからの読み込み）はGTK+イベントに変換され、アプリケーションのウィジェットオブジェクト上のGLibシグナルとして発せられる。

 macOSのループ 
macOSではスレッド毎に1つのCFRunLoopがあり、それに任意個のソース（イベント源）とオブザーバ（ハンドラ）を対応させることができる。そのループがメッセージのキューイングとディスパッチを行い、それを通してソースとオブザーバがやりとりする。

CocoaではCFRunLoopがNSRunLoopに抽象化されており、任意のメッセージをキューイングして任意のオブジェクトにディスパッチできる。

 Androidのメッセージループ 
Java言語向けのAndroid SDKにおけるアプリケーションフレームワークには、メッセージを表現するandroid.os.MessageクラスMessage  |  Android Developers、メッセージキューを表現するandroid.os.MessageQueueクラスMessageQueue  |  Android Developers、メッセージループの実装であるandroid.os.LooperクラスLooper  |  Android Developers、メッセージの送信と処理を担当するandroid.os.HandlerクラスHandler  |  Android Developersなどが用意されている。ただし、Looper.loop()メソッドの実装に使われているMessageQueue.next()メソッドなどがAPIとして公開されていないためcore/java/android/os/Looper.java - platform/frameworks/base - Git at Google、メッセージループを独自に実装することはできない。

Android NDKでは、POSIXパイプとALooper関連APILooper  |  Android NDK  |  Android Developersを利用して、ネイティブスレッド上にメッセージループを独自に実装することができる。NativeActivityのサンプルには、ALooperを利用したメッセージループの実装が含まれているサンプル: native-activity  |  Android NDK  |  Android Developers。

 用途 

 メインループ以外の用途 
イベントループは下記の例のように入れ子構造が可能となっている。これによりイベントループを途中においてその前後に初期化と終了処理を置くということが可能で次の章で上げるOS機能の他、イベントループを下記のPhase 1〜Phase 3の様に複数のループに分割しSplash表示-更新通知-本編表示といった推移などに応用することができる。

※下記は例示用のため実用性は無い。MSG msg;

PostMessage( nullptr, 1 + WM_USER, 0, 0 );
while( 0 < GetMessage( &msg, NULL, 0, 0 ) )
{
    // Phase 1
    {
        std::clog << "Begin Phase 1" << std::endl;
        unsigned count = 0;

        // 条件が成立するまでイベントループ
        while( 0 < GetMessage( &msg, NULL, 0, 0 ) )
        {
            if( /*何らかの条件*/ )
            {
                ++count;
                continue;
            }

            PostQuitMessage( EXIT_SUCCESS );
        }

        std::clog << "End Phase 1" << std::endl;
    }

    // Phase 2
    {
        std::clog << "Begin Phase 2" << std::endl;

        unsigned count = 0;
        while( 0 < GetMessage( &msg, NULL, 0, 0 ) )
        {
            if( /*何らかの条件*/ )
            {
                ++count;
                continue;
            }

            PostQuitMessage( EXIT_SUCCESS );
        }

         std::clog << "End Phase 2" << std::endl;
   }

    // Phase 3
    {
        std::clog << "Begin Phase 3" << std::endl;

        unsigned count = 0;
        while( 0 < GetMessage( &msg, NULL, 0, 0 ) )
        {
            if( /*何らかの条件*/ )
            {
                ++count;
                continue;
            }

            PostQuitMessage( EXIT_SUCCESS );
        }

         std::clog << "End Phase 3" << std::endl;
   }

    PostQuitMessage( 0 );
}

 モーダルウィンドウの実装 
モーダルウィンドウを起動する関数は、モーダルウィンドウを閉じるまで呼び出し元に制御を返さないが、これはその関数の中でイベントループを実行しているためである。イベントループを挟むことによりモーダルウィンドウ以外のウィンドウに送られるイベントを防ぎ、他のウィンドウの操作を防いでいる。また、メッセージボックスのように選択してすぐ終了というウィンドウでは、選択するまでメッセージボックスを表示した関数の復帰を防ぎその選択結果を関数の戻り値として返すための仕組みとしても働いている。

 ドラッグ・アンド・ドロップ 
ドラッグを開始したウィンドウ以外の上をカーソルが移動している間、横断的にイベントを監視しどこにドロップされてもドラッグ状態を解除できるようにするためにドラッグ開始の関数の中で使われている。また、ドラッグ開始の関数はドラッグ完了まで処理を返さないことで、ドラッグ開始の関数の後ろにドラッグ前に確保した資源を開放する処理をかけるようになっている。なお、稀にウィンドウの外にドラッグするとドラッグを中止したりドラッグ状態が戻らないようなソフトウェアが見られるが、イベントループを使わずに独自に各ウィンドウのイベントを監視して実装しているためである。

 脚注 

 注釈 

 出典 

 関連項目 
 メッセージ (コンピュータ)
 イベント (プログラミング)
 イベント駆動型プログラミング
 プロセス間通信
 非同期IO
 モーダルウィンドウ

外部リンク
 Meandering Through the Maze of MFC Message and Command Routing
 WindowProc callback function (Windows) | Microsoft Docs

Category:制御構造