HaskellのスレッドシステムとSTMについて その1
概要
HaskellのスレッドシステムとSTMについて調べたので、ここにまとめます。
Haskellのスレッドシステムは、予想よりも複雑でした。 Haskellの世界で閉じた処理ならば、比較的簡単なのですが、 FFI(Foreign Function Interface: 外部のCの関数を呼び出す仕組み)を使うと、 とたんに複雑になってしまいます。
一度に書くと、量が多いため、数回に分けて書こうと思っています。
注意
今回の調査は、環境に強く依存します。
- GHC 6.8.2
- Mac OS X 10.5.1 (32bit code)
の環境で調べた結果に基づいています。 特にGHC以外の処理系では、このような結果にならないかもしれません。 [*]
[*] | GHC拡張を使っているコードもあるので、それ以前に実行できないかもしれません。 |
Linuxや*BSDの環境には、それほどコードを変更せずに適用出来ると思いますが、 Windows環境には適用するのが難しい所もあります。 また、32bit/64bitの違いでもコードを変更する必要があるかもしれません。
Haskellのスレッドシステムについて
Haskellのスレッドシステムには、2つのスレッドが存在します。 forkIO と forkOS という2つの関数を用いて生成することができます。
forkIO :: IO () -> IO ThreadId forkOS :: IO () -> IO ThreadId
どちらの関数も、第一引数の IO () のアクションを評価するスレッドを生成し、スレッドIDを返します。 やっている事は同じなのですが、2つの関数では生成されるスレッドの性質が異なっています。 また、 forkOS は必ずしもサポートされているわけではありません [1] 。
スレッドの性質の違いについては、ややこしいので後回しにします。 しばらくは forkIO の方しか使いません。
Haskellの参照型について
forkIO の定義を見てもわかるように、 スレッドで実行されるアクションは値を返すことができません。 しかし、時には別スレッドで実行された結果を受け取りたいこともあります。 そんな時に使うのが、参照型です。
Haskellにはいくつか参照型があり、それぞれ異なる性質を持ちます。代表的なのは以下のものです。
IORef: | ただの参照型。一番高速だが、ロックが制限的。 |
---|---|
Mvar: | ロックの使える参照型。 |
Chan: | 同時に複数のスレッドから操作しても破綻しない FIFO キュー。 |
TVar: | STMという特殊なモナドの中でのみ変更できる参照型。ロックフリー。 |
IORef は、高速ですが、 atomicModifyIORef という関数でしか アトミック性を保証出来ません。 また、複数の変数を同時にロックすることができません。
続いて MVar です。これは、ロックのかけられる IORef と捉えることができます。 また、 Chan はこの MVar を用いて実装されているキューです。 複数のスレッドから操作しても壊れることはないように設計されています [2] 。
最後のが、今回中心的に取り上げる、 TVar です。 これは STM モナドの中でのみ操作することができます。 STMとは、"Software Transactional Memory"の略です。 Software Transactional Memoryとは、RDBのそれの様に、 メモリ変更が、完全に成功したか、何もしていないかの二択の状態になるメモリ操作のことです。 一部だけ変更されるという、中途半端な状態を取らないようになります。 また、ハードウエアで実装されたものは、"Hardware Transactional Memory"と呼ばれます。
さっそく動かしてみる
ソースコード
さっそく、スレッドを動かしてみます。
import Control.Concurrent import Control.Monad (forever) -- threadDelay in milliseconds. -- `delay 1000' means that this thread sleeps 1 sec at least. delay :: Int -> IO () delay = threadDelay . (* 1000) -- Do the first argument action every `time' ms infinitely. every :: Int -> IO a -> IO () every time io = forever $ delay time >> io main :: IO () main = do forkIO $ every 1000 $ putStrLn "a" forkIO $ every 500 $ putStrLn "b" return ()
2つのユーティリティー関数とmain関数から成るプログラムです。
delay: | 標準関数の threadDelay はマイクロ秒を引数に取ります。 桁が増えて扱いがよくないので、ミリ秒を引数に取る関数を作ります。 |
---|---|
every: | time ミリ秒ごとに io を実行する関数です。永遠に止りません。 |
main 関数では、2つのスレッドを起動し、 片方のスレッドでは1秒間隔で"a"を出力します。 もう一方のスレッドでは0.5秒間隔で"b"を出力します。
このコードでは、2つのスレッドが同時に putStrLn を実行すると、 表示がおかしくなる場合がありますが、 このコードの目標が「とりあえず動かす」なので無視しています。
実行結果
$ ./mt $
生成されたファイルを実行しましたが、すぐに終了していまいました。 Haskellでは、生成したスレッドの終了を待つ他の言語で言う join という仕組みがありません。 今回のサンプルでは、一切スレッドの終了を待っていないため、 メインスレッドが終了すると、プログラム全体が終了していまいます。
他のスレッドを待つ方法については、また次回…。
- Category(s)
- Haskell
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/mizyo/haskell306e30b930ec30c330b730b930c630e03068stml306b306430443066-305d306e1/tbping