メッセージ通信チャンネルを扱う型

 ここまでのSystem.Processの説明では外部プログラムの使用を前提にしていました。しかし,メッセージ通信が有用なのは外部プログラムを利用する場合だけではありません。非同期メッセージ通信を実現するために使われるバッファが,並行処理を行うプログラムの動作を記述するのに都合がよいこともあります。その場合,スレッドで共有メモリを直接利用する代わりに,メッセージ通信を模したデータ構造を使うことになります。ここからはそうした例について見ていきます。

 GHCやHugsではメッセージ通信チャンネルをエミュレートする型がライブラリとして用意されています。これを使ってスレッド間でメッセージ通信ができます。通信チャンネルをエミュレートする型には,MVarを使って実装したChanとTVarを使って実装したTChanがあります。ChanはControl.Concurrent.Chanモジュール,TChanはControl.Concurrent.STM.TChanモジュールで定義されています。Control.Concurrent.ChanモジュールはControl.Concurrentモジュール,Control.Concurrent.STM.TChanモジュールはControl.Concurrent.STMモジュールから利用できます。以下にTChanの定義を示します。

data TChan a = TChan (TVar (TVarList a)) (TVar (TVarList a))

type TVarList a = TVar (TList a)
data TList a = TNil | TCons a (TVarList a)

newTChan :: STM (TChan a)
newTChan = do
  hole <- newTVar TNil
  read <- newTVar hole
  write <- newTVar hole
  return (TChan read write)

newTChanIO :: IO (TChan a)
newTChanIO = do
  hole <- newTVarIO TNil
  read <- newTVarIO hole
  write <- newTVarIO hole
  return (TChan read write)

writeTChan :: TChan a -> a -> STM ()
writeTChan (TChan _read write) a = do
  listend <- readTVar write -- listend == TVar pointing to TNil
  new_listend <- newTVar TNil
  writeTVar listend (TCons a new_listend)
  writeTVar write new_listend

readTChan :: TChan a -> STM a
readTChan (TChan read _write) = do
  listhead <- readTVar read
  head <- readTVar listhead
  case head of
    TNil -> retry
    TCons a tail -> do
        writeTVar read tail
        return a

 TChanの実装は,読み込みのための先頭のポインタと書き込みのための末尾のポインタを保持したメッセージ・キューになっています。newTChanやnewTChanIOで空のキュー(すなわち通信チャンネル)を作成します。writeTChanでメッセージの送信としてキューの末尾に要素を追加し,readTChanがwriteTChanによって書き込まれた要素を先頭から順に読み込みます。このように先入れ先出し(FIFO:First In, First Out)方式でメッセージの送受信を行います。その結果,メッセージ通信はブロックされることのない非同期処理として行われます。

 ただし,非同期処理といっても,通信チャンネルが空のときにはメッセージ送信が行われるまでメッセージの受信がブロックされます。TChanには,通信チャンネルの中身が空かどうかを確認するisEmptyTChanという関数があります。

isEmptyTChan :: TChan a -> STM Bool
isEmptyTChan (TChan read write) = do
  listhead <- readTVar read
  head <- readTVar listhead
  case head of
    TNil -> return True
    TCons _ _ -> return False

 また,readTChanで読み取った内容を削除したくないときのために,読み込んだ要素を先頭に戻すunGetTChanという関数が用意されています。

unGetTChan :: TChan a -> a -> STM ()
unGetTChan (TChan read _write) a = do
   listhead <- readTVar read
   newhead <- newTVar (TCons a listhead)
   writeTVar read newhead

 ただ,TChanは素朴なFIFOとして実装されているため乱用は避けましょう。unGetTChanで頻繁に通信チャンネルの中身を変更すると,値が更新されるタイミングによってはうまく動作しなくなってしまう可能性があります。

 TChanの内部の値を細かく制御したり,複数箇所でTChanの中身を共有したりしたい場合には,目的ごとに別の通信チャンネルを作成するか,dupTChanを使ってメッセージの送信を複数の通信チャンネルに「ブロードキャスト(マルチキャストともいう)」するとよいでしょう。

dupTChan :: TChan a -> STM (TChan a)
dupTChan (TChan read write) = do
  hole <- readTVar write  
  new_read <- newTVar hole
  return (TChan new_read write)

 dupTChanはこのように通信チャンネル(キュー)の末尾のポインタをコピーして新しい通信チャンネルを作成します。これにより,いずれかの通信チャンネルに対してwriteTChanで書き込まれた要素を,両方の通信チャンネルで共有して使用できます。一方,readTVarは値を読み取った後,先頭のポインタを付け替えるだけなので,別の通信チャンネルには影響しません。

 あとは,型をメッセージ・タグ(message tag)とみなして関心のあるメッセージだけ受け取るようにすれば,通信チャンネルの中身を書き換えなくても柔軟な処理を行えます。

msg <- readChan ch
case msg of
  MouseMove pt -> doSomething pt
  -- 再帰させることで次のメッセージを取得しにいくことも可能
  _ -> return ()