メッセージ通信チャンネルの実用例

 メッセージ通信を利用する具体例を挙げていきましょう。

 並行プログラミングにおいてメッセージ通信のイメージを使うことが有用な典型例は,第11回で紹介したパイプライン並列などの並列化手法を実装する場合です。並列Haskellを使ったパイプライン並列化では,Haskellは遅延評価なので特にスレッド間でのデータのやり取りを行うための仕掛けは必要ありませんでした。ところが,Haskellを使っていても,並行プログラミングのようなI/Oアクションを使った処理では遅延評価は働きません(参考リンク)。したがって,単純な並列化ができないため並行Haskellを使って並列処理を記述する必要のある場合では,データのやり取りを行うための仕掛けが必要となります。

 パイプライン並列化を実現するためには,データのやり取りを行う際に二つの条件があります。まず,データをやり取りするスレッドがそれぞれ非同期に動かなければなりません。それぞれのスレッドが同期的に動作すると並列ではなくなってしまうので当然ですね。もう一つは,前のスレッドがまだ作業中であるために現在のスレッドで処理するべきデータが存在しない場合,現在のスレッドをブロックして待つ必要があるということです。パイプライン並列化によってそれぞれの処理が非同期的に動くようになれば,処理速度によっては現在処理するべきデータがまだ存在しない場合が生じるからです。

 非同期のメッセージ通信では,これらの要求を満たすためのバッファを持ちます。このため,メッセージ通信のイメージを使ったり,メッセージ通信チャンネルを擬似的に実現するTChanのような型が有用なのです。

 ほかに,どんな場合が考えられるでしょうか?

 メッセージ通信チャンネルは,FIFOのようにメッセージをほぼ発生順に保持していく形のバッファを持つのが一般的です。このため,非同期に発生したメッセージを一個所に集約する処理を書きやすいと考えられます。そのような処理の例としては,ネットワークやGUIなどのイベント(事象)駆動方式のプログラムが考えられます。

 一般に,イベント駆動プログラミングで扱うイベントは,いつどこで生じるかわからない非同期のものです。一方,プログラム本体は,イベントの発生順序に従って処理を行います。もし,発生したイベントをイベントの発生順どおりに取得することのできる関数があれば,イベント駆動方式のプログラムをきれいに記述できます。この目的にはメッセージ通信チャンネルが適しています。

 具体的な例を見てみましょう。第1回で紹介した「The Haskell School of Expression: Learning Functional Programming through Multimedia」(SOE)という本の最新版のサンプル・コード(2007年10月現在)を以下に示します(参考リンク)。

data Window = Window {
  graphicVar :: MVar (Graphic, Bool), -- boolean to remember if it's dirty
  eventsChan :: Chan Event
}

openWindowEx :: Title -> Maybe Point -> Maybe Size -> RedrawMode -> IO Window
openWindowEx title position size (RedrawMode useDoubleBuffer) = do
  let siz = maybe (GL.Size 400 300) fromSize size
  initialize
  graphicVar <- newMVar (emptyGraphic, False)
  eventsChan <- newChan
  GLFW.openWindow siz [GLFW.DisplayStencilBits 8, GLFW.DisplayAlphaBits 8] GLFW.Window
  GLFW.windowTitle $= title
  modifyMVar_ opened (\_ -> return True)
  GL.shadeModel $= GL.Smooth
  -- enable antialiasing
  GL.lineSmooth $= GL.Enabled
  GL.blend $= GL.Enabled
  GL.blendFunc $= (GL.SrcAlpha, GL.OneMinusSrcAlpha)
  GL.lineWidth $= 1.5

~ 略 ~

  let motionCallback (GL.Position x y) =
        writeChan eventsChan MouseMove { pt = (fromIntegral x, fromIntegral y) }
  GLFW.mousePosCallback $= motionCallback
      
  GLFW.charCallback $= (\char state -> do
    writeChan eventsChan (Key {
        char = char,
        isDown = (state == GLFW.Press) }))

  GLFW.mouseButtonCallback $= (\but state -> do
    GL.Position x y <- GL.get GLFW.mousePos
    writeChan eventsChan (Button {
        pt = (fromIntegral x, fromIntegral y),
        isLeft = (but == GLFW.ButtonLeft),
        isDown = (state == GLFW.Press) }))

  GLFW.windowSizeCallback $= writeChan eventsChan . Resize
  GLFW.windowRefreshCallback $= writeChan eventsChan Refresh

~ 略 ~

  return Window {
    graphicVar = graphicVar,
    eventsChan = eventsChan
  }

---------------------------
-- Event Handling Functions
---------------------------

data Event = Key {
               char :: Char,
               isDown :: Bool
             }
           | Button {
              pt :: Point,
              isLeft :: Bool,
              isDown :: Bool
             }
           | MouseMove {
               pt :: Point
             }
           | Resize GL.Size
           | Refresh
           | Closed
  deriving Show

getWindowEvent :: Window -> IO Event
getWindowEvent win = do
  event <- maybeGetWindowEvent win
  maybe (getWindowEvent win) return event

maybeGetWindowEvent :: Window -> IO (Maybe Event)
maybeGetWindowEvent win = do
  GLFW.swapBuffers
  noEvents <- isEmptyChan (eventsChan win)
  if noEvents then return Nothing
              else do event <- readChan (eventsChan win)
                      return (Just event)

 GLやGLFWという修飾名は,OpenGLやGLFWなどのライブラリのモジュールを使っていることを示すものです。ここでは,画像の描画にOpenGL,GUIのイベントを扱うのにGLFWというライブラリを使っていることがわかれば十分です。

 今回の話題に関係するのは,GUIのイベントを扱っているGLFWのコールバック(call-back)関数を呼び出している部分です。openWindowExでは,ウィンドウを作成する際に**Callback関数と$=演算子を使って,イベントの発生時にすべき処理を登録しています。イベントの発生を一つにまとめて扱うために,ここでの処理の多くは,イベントに応じたEvent型の値をeventsChanという通信チャンネルに送信しています。

 こうして送信したメッセージを,getWindowEventを使って順々に取得します。getWindowEventの内部では,maybeと(maybeGetWindowEvent内部の)isEmptyChanを組み合わせることで,次に処理するべきイベントが存在しないとき(イベントを保持するメッセージが空のとき)には,ループで待ち続けるようになってます。

 このような仕組みにより,イベント駆動方式でのプログラムがとてもシンプルに実現できています。getWindowEventでは,イベントの発生に伴う処理の分岐とEventの値に応じた処理の分岐を一対一に対応させて書くことができます。これまでのように複雑に入り混じったコールバック処理の海をさまよう必要はありません。

 同様に,ネットワーク・プログラミングにおいても,メッセージ通信のイメージをうまく使える場面があります(参考リンク)。

runInteractiveProcessの様々な派生系

 runInteractiveProcessには,runInteractiveCommand以外にも様々な派生系が存在します。「一つのプロセスに一つの処理を行わせる」といった対話的ではない処理を行う場合には,runInteractive**の代わりにrun**を使用できます。

Prelude System.Process> :t runProcess
runProcess :: FilePath
-> [String]
-> Maybe FilePath
-> Maybe [(String, String)]
-> Maybe GHC.IOBase.Handle
-> Maybe GHC.IOBase.Handle
-> Maybe GHC.IOBase.Handle
-> IO ProcessHandle
Prelude System.Process> :t runCommand
runCommand :: String -> IO ProcessHandle

 ただし,型が異なるため,使用方法が少し異なります。runProcessでは標準入力,標準出力,標準エラーに使用するハンドルを,返り値ではなく引数として与える必要があります。また,runCommandではこうした標準入出力のハンドルを使用できません。

 標準入出力へのハンドルが不要なら,標準Haskellで定義されているSystem.Cmdモジュールのsystem関数を使うこともできます(参考リンク)。

Prelude System.Cmd> :t system
system :: String -> IO GHC.IOBase.ExitCode

 systemは,runProcessとwaitForProcessを合わせたような処理を行います。systemを使う場合には,コマンドの実行が終了するまで待つしかありません。一見不便ですが,標準出力による結果が不要で,かつ処理の終了が保証されている場合には,簡潔な記述ができます。

 またSystem.Cmdモジュールには,シェルではなくOSに直接コマンドを処理させるためのrawSystemという関数もあります。run**Processなどと同じく,コマンドに対する引数を文字列のリストとして渡します。

Prelude System.Cmd> :t rawSystem
rawSystem :: String -> [String] -> IO GHC.IOBase.ExitCode

関数対話処理シェル標準入出力のハンドルの利用処理の終了待ち
runInteractiveProcess経由しないしない
runInteractiveCommand経由するしない
runProcess×経由しないしない
runCommand×経由する×しない
rawSystem×経由する×する
system×経由しない×する


著者紹介 shelarcy

 2007年9月6日に「Haskellと並列プログラミング」という題目で講演しました(参考リンク)。この講演では,これでまでの並行/並列プログラミングに対する説明を「なぜHaskellで並列プログラミングを行うべきのか」という切り口で語ってみました。

 この講演のプレゼン資料は,上記のリンク先で公開しています。これまでの回で説明してきた話がコンパクトにまとまっているので,頭の中が今ひとつ整理できていない方は,この資料を読むとよいと思います。