スレッドリーク

これはHaskellスペースリーク Advent Calendar 2015の14日目の記事です。

スレッドリークとは一般的に、終了させることを忘れたスレッドが残り続けることを言う。これは終了させ忘れたのが悪いという他ない。一方で、GHC では、スレッドを終了させたにもかかわらず、スレッドのメモリ領域がCGに回収されない事態が起こりうる。この記事は、この二番目のスレッドリークについて説明する。

ThreadId

ThreadIdは、名前からしても、以下のように表示させても、Ordのインスタンスであるところからも、単なる番号のように思える。

> forkIO (return ()) >>= print
ThreadId 88

しかし、ThreadIdのマニュアルを注意深く読むと、以下のようなことが書いてある。

GHCの実装では、ThreadIdは本質的にスレッドに対する参照である。
これは、ThreadIdを捨てなければ、GCがスレッド自体を回収できないことを意味する。
このおかしな仕様は、将来修正されるだろう。

なんということだろう。ThreadId をどこかに保持していると、スレッドは終了しても開放されないのだ。これは、実際に Warp で問題となった。

弱い参照

これを解決するには、ThreadIdという強い参照を弱い参照に変換すればよい。このための関数である mkWeakThreadId のシグニチャは以下の通り:

mkWeakThreadId :: ThreadId -> IO (Weak ThreadId)

ThreadIdの代わりに、Weak ThreadId を保持するわけだ。Weak a は、Eq でも Ord でもないので、リストぐらいにしか格納できないことに注意。

Weak ThreadId から ThreadId を取り出すには、System.Mem.Weak の deRefWeak を使う。

deRefWeak :: Weak v -> IO (Maybe v) 

deRefWeakは、弱い参照の先に実際にデータがあれば Just を、そうでなければ Nothing を返す。ThreadId と一緒に使うコードの例を以下に示す。

do mtid <- deRefWeak wtid
   case mtid of
      Nothing  -> return ()
      Just tid -> killThread tid

まとめ

Haskellの一般的なデータ構造は、明示的には解放できない。しかし、スレッドは終了することで解放できる特殊なデータ構造である。他の言語からすれば当たり前なこのデータ構造には、他の言語が苦しめられているのと同種の危険性がある。