Personal tools
You are here: Home ブログ n.fujita Blog HaskellのスレッドシステムとSTMについて その3
Document Actions

HaskellのスレッドシステムとSTMについて その3

STMとロックについて

だいぶん間が開いてしまいましたが、その3です。

STMモナドによる操作は、ロックフリーです。 synchronize だとか lock だとか unlock だとかいうキーワードを書く必要はありません。 しかし、それは Haskell のコードからの視点のようです。 内部的にはロックを使っています。 そこで、ロックを実感するために、このようなコードを実行してみます。 import宣言と、ここにない関数はその1、その2から持ってきてください。

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

threadA :: TVar Int -> IO ()
threadA v =
    do n <- atomically $ readTVar v
       print n
       atomically $ writeTVar v (n + 1)

threadB :: TVar Int -> IO ()
threadB v = atomically $ writeTVar v $ fib 40

main :: IO ()
main = do c <- newCounter
          v <- newTVarIO 0
          fork c $ every 500 $ threadA v
          fork c $ threadB v
          waitCounter c 2

fib は有名なフィボナッチ数列です。 時間がかからないと意味がないので、わざと遅い実装にしています。 threadAfib 40 を計算して、2つのスレッドで共有している変数 v に結果を代入しています。 threadBv の値を出力して v を1増やしています。

このコードを実行すると、

0
1
2
  (中略)
102334155
102334156
102334157

このような結果が期待されます。 しかし、このような結果にはなりません。 実際には、

102334155
102334156
102334157
102334158
102334159

のようになります。0, 1, 2 ...の値は出力されません。

atomicallyとロック

atomically は、内部的にTVarをロックしているようです。 そのロックの影響で、前章でのサンプルコードは期待されるような結果になりません。 atomically はトランザクションをコミットする時に関係するTVarを全てロックします。 ロックされているTVarは他のスレッドから参照することも、書き込むこともできません。

また、Haskellは遅延評価をする言語です。 atomically がTVarのロックを取得した後に、 v に書き込むための値を計算します。 ( readTVarwriteTVar をする時にロックしているわけではありません。) しかし、 fib 40 の計算には非常に時間がかかります。 TVarをロックした後に計算しているため、``v`` を読もうとしている threadA も長時間ブロックされてしまいます。

正格評価をしてみる

threadB の内容を変更し $ の代りに正格性のある $! を使ってみます。

threadB :: TVar Int -> IO ()
threadB v = atomically $ writeTVar v $! fib 40

長いので変化のある所だけにしますが、結果はこのようになります。

18
19
20
102334155
102334156
102334157

今回は期待通りに動いています。 writeTVar する時に fib 40 の計算をすましているので、 atomically の内部で計算待ちでロックしなくなります。

しかし、手元の環境では -O と最適化フラグを付けてコンパイルすると、 遅延評価版と同じ結果になってしまいます。 インライン展開が悪さをしているようです。

{-# NOINLINE fib #-}
fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n - 1) + fib (n - 2)

このように、 NOINLINE プラグマを付けると、最適化を行っても大丈夫でした。

でも…

atomically の中で正格評価を行うのは時に問題となります。 プログラムをこのように書き換えてみます。

{-# NOINLINE fib #-}
fib :: Int -> Int
fib n = trace ("fib " ++ show n) $ fib' n
    where fib' 0 = 0
          fib' 1 = 1
          fib' n = fib' (n - 1) + fib' (n - 2)

threadA :: TVar Int -> IO ()
threadA v = do x <- atomically modify
               print x
    where modify = do x <- readTVar v
                      writeTVar v $ x + 1
                      return x

threadB :: TVar Int -> IO ()
threadB v = atomically $ do
              x <- readTVar v
              writeTVar v $! fib x

main :: IO ()
main = do c <- newCounter
          v <- newTVarIO 35
          fork c $ every 50 $ threadA v
          fork c $ threadB v
          waitCounter c 2

まず、変更内容についてですが、 fib が実行された時に、引数の n の値をトレースするようにします。 threadA は次々と v の内容を変化させ、 threadBv の内容を使って fib を計算しています。 main の内容は変化していませんが、 threadA の実行間隔が短い方がわかりやすいので、変更しています。

このプログラムを実行すると、このようになります。 fib が何度も呼び出され、いつまでたっても計算が終りません。

fib 35
35
fib 36
36
fib 37
37
fib 38
38
fib 39
39
fib 40
40

threadBfib を計算中に threadA によって v の内容が書き換えられるため、 threadBatomically が何度も再実行されます。 遅延評価( $ を使う)コードにしておくと、 v のロックを得てから fib を計算するため、 何回も実行されることはありません。(そのかわり threadA の実行は止まりますが。)

外で計算する

threadB の内容を書き換えて、 atomically の外で fib を計算するようにすると、 fib が何度も計算されることはなく、 threadA の実行も止まりません。 (ここで、 $! を使って正格評価にしないと、意味がありません。)

threadB :: TVar Int -> IO ()
threadB v = do x <- atomically $ readTVar v
               y <- return $! fib x
               atomically $ writeTVar v y

ロックフリーと言えども

ロックフリーと言えども、 STM を使う上ではロックに気を付ける必要がありそうです。 ロックの開放忘れを気にしなくてもよかったり、 ロックの順番を気にしなくてもよかったり、STMモナドが合成できたり、いいこともありますが、 ややこしいバグを発生させる可能性もありそうです。

その4

その4に続くかもしれません。

Category(s)
Haskell
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/mizyo/haskell306e30b930ec30c330b730b930c630e03068stm306b306430443066-305d306e3/tbping

Re:HaskellのスレッドシステムとSTMについて その3

Posted by aljee at 2008-02-16 08:52
こんにちは。少し気になるところがあるので指摘させて下さい。
最初の例ですが、これはTVarをロックした後にfib 40を計算している訳ではありません。
まず、threadBのatomicallyは即座に終了し、vには未評価のfib 40が入った状態になります。
500ms後、これをthreadAが取り出し、printで表示する段になって、初めてfib 40が計算されるので、このような結果になります。
print nの直前に、putStrLnなどでメッセージを表示させてみると、どこに時間が掛かっているか分かりやすいと思います。

Re:HaskellのスレッドシステムとSTMについて その3

Posted by n.fujita at 2008-09-03 00:38
aljeeさん、指摘ありがとうございます。
そして6ヶ月も放置してすみません。

なるほど。そういう処理をしているのですね。
TVarに入れられる値は計算されてから入れられるだろう という思い込みをしていました。

Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)
(Required)
(Required)
This helps us prevent automated spamming.
Captcha Image


Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.