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