concurrent haskell

This entry was posted by on Tuesday, 13 June, 2006
>Concurrent Haskell の動作をチェックしてみた。まず、いちばん基本となる関数が forkIO と forkOS で、どちらも IO () -> IO ThreadId の型を持つ。違いは、 forkIO は仮想機械レベルの並列計算なのに対して、 forkOS は OS のネイティブスレッドを使う。そのため、 forkOS の方は ghc を -threaded でコンパイルしないと動かないといった制約がある(runhaskell や ghci では動作しないようになっている)。そういった差はあるが、下記のレベルではどっちでも差はない。 > >というわけで、まずは動作例。 > >do forkOS (mapM_ (putStrLn "child") [1..10])
mapM_ (putStrLn "parent") [1..10]
> >こいつは、子スレッドが10回「child」と書き出し、同時並列的に親が「parent」と書き出す。これは悪くないのだが、親は子の終了を待たないので、実際にこれをやると、 child がぜんぶ出力される保証がない(forkIO を使って対話環境で実行すると、親スレッドが終了したところでプロンプトに戻ってしまい、プロンプトで何かやると、次の計算に、前の子スレッドの実行が混ざるようになる)。 > >これじゃ困るのでスレッドの終了を待ちたい。ただ、 pthread の API と違って、 fork* の返り値である ThreadId はその目的には使えない。これには MVar を使う。 MVar は、「変数の置き場」みたいなもので、 IORef に似ているが、「何もない」状態にすることができる(IORef (Maybe a) に似てるか)。 MVar を読むと変数が「取られ」空になったりする。もし空なら、そのスレッドの動作はブロックするから、これを使えば子スレッドの終了を待つことができる。 > >do v <- newEmptyMVar
forkOS (mapM_ (putStrLn.("child "++).show) [1..10] >> putMVar v 0)
mapM_ (putStrLn.("parent "++).show) [1..10]
takeMVar v
> >newEmptyMVar は、空っぽの MVar を作る(初期値を指定する newMVar などもある)。 MVar に値を「置く」のが putMVar で、読み込むのが takeMVar だ。実際には、読み込むが値を「取る」ことをしない readMVar というのもある。 > >さて、こうやってスレッド間で通信をするわけだが、 MVar は1つの要素しか持たないという制約があるから、新しい値を置こうとするとブロックしたり、古い値を消すハメになったり、といった問題がある。なので、どんどん値を追加していく、いわばチャンネルが欲しいこともある。そのためのものが Chan だ。 > >do ch <- newChan
forkOS (writeList2Chan ch [1..100])
loop ch
where loop ch = isEmptyChan ch >>= flip unless (readChan ch >>= print >> loop ch)
> >Chan にも MVar と同じような API が用意されているが、特徴的なのは writeList2Chan というのがあることか。リストをまるごとチャンネルに書き出す処理を行っている。どうでもいいが、このサンプルで writeList2Chan を forkOS する意味はあんまりない。 > >ここで、1つのチャンネルを複数のスレッドから読み込むことを考える。同じものを読み込むわけだから、一方のスレッドで読むと、もう一方のスレッドでは、その次の要素が読まれる。つまり、値を奪いあうわけだ。だから、こうなる。 > >do v <- newEmtpyMVar
ch <- newChan
forkOS (writeList2Chan ch [1..10])
forkOS (loop "child" ch >> putMVar v 0)
loop "parent" ch
takeMVar v
where loop suf ch = isEmptyChan ch >>= flip unless (readChan ch >>= putStrLn.(suf++).show >> loop suf ch)
> >parent と child が交互にあらわれるが、数値そのものは1〜10が1度ずつしか出ないことがわかるだろう。 > >さて、このままだと、複数のスレッドに同時に値を伝えるようなブロードキャストをするのがメンドくさい。この目的のため、チャンネルを複製することができる。 > >do v <- newEmptyMVar
ch <- newChan
ch' <- dupChan ch
forkOS (writeList2Chan ch [1..10])
forkOS (loop "child" ch' >> putMVar v 0)
loop "parent" ch
takeMVar v
(loop の定義は略)
> >child と parent のそれぞれが 1〜10を出力することがわかるだろう。 dupChan は空のチャンネルが作る。しかし、作られた新しいチャンネルともとのチャンネル、どちらかに書き込まれたデータはどちらのチャンネルからも読めるようになる。書き込み側では ch に書き込んでいるが、 ch' からもデータを読めるようになるわけだ。 > >ところで、 writeList2Chan は最近のマシンでは一瞬で終わってしまうので、 > >forkOS (writeList2Chan ch [1..10])
ch' <- dupChan ch
> >と順序を変えると、 dupChan で複製する前に書き込みが完了してしまって、 ch' は空のチャンネルになってしまい、 child は何も出力しない。 > >そういえば Control.Concurrent.STM というのもあるのだった。こちらは未見。 >

Comments are closed.