Let's start Scheme

2014-09-02

let-method

Sagittariusはlet-methodという総称関数のスコープを限定する構文をもっているのだが、これとマルチスレッドが絡むとうまく動かないという話。

例えば以下のようなのを考えてみる。
(import (rnrs) (clos user) (srfi :1) (srfi :18) (srfi :26))

(define-generic local)

(define (thunk)
  (thread-sleep! 1)
  (let-method ((local (a b c) (print a b c)))
    (thread-sleep! 1)
    (local 1 2 3))
  (local "a" "b" "c"))

(let ((ts (map thread-start! (map (cut make-thread thunk <>) (iota 10)))))
  (for-each thread-join! ts))
let-methodの外側で呼ばれるlocalメソッドはどのような場合でもエラーを投げることが期待されるのだが、実はこれが期待通りには動かない。理由はいたって簡単で、スレッドAが二度目のlocal呼び出しをする際にスレッドBがlet-method内であればlocal自体は特殊化されたメソッドを持っていることになる。あまり使わない構文な上に、マルチスレッド環境のことなど頭から抜け落ちていたのでこういうことに気付かなかった。

では、どうするかということもついでに考えてみる。現状では総称関数はスレッド関係なくグローバルに影響を与えるのだが、こいつをスレッド毎にしてしまえばいいだけの話ではある。総称関数だけを特別視するというのは多少気持ち悪い部分もあるのだが、パラメタと同じでVMにそれ様のレジスタを追加しスレッドが作成されたらコピーすればいいという話になる。そうすることで大元の総称関数は変更されない。

問題はdefine-genericもdefine-methodも単なるマクロで、内部的には総称関数を作ってdefineで定義しているだけという部分と、VMは識別子を一度参照するとGLOCに置き換えるという点である。最初の問題は実はそんなに大きくなく、束縛を作成する際に値が総称関数であれば現在のVMに追加してやればいい。(そもそも子スレッドで束縛を作るというのはいかがなものかという話もあるのだが。) GLOCの問題はGLOCがコンパイルされたコードに埋め込まれつつ、この中身が基本変更されないという前提があることに起因する。GLOCがあるとコピーされた総称関数が参照できないということになる。

とりあえず思いつく限りでは2つ解決策がある。
  • 総称関数の参照はGLOCにしない
  • 総称関数の呼び出し時にうまいこと解決する
一つ目は全体のパフォーマンスに影響を与えそうではあるが、常に参照を解決するようにして束縛を探す際にコピーされた総称関数を返してやればよさそうである。
二つ目はVMのレジスタが大元の総称関数とコピーされたものの紐付けを持っておき、呼び出し時にコピー側に解決するというもの。単なる参照として別の手続きに渡された際にどう解決するかというもの。GREFが走るたびにチェックを設けていてはGLOCの意味がない気がするが、総称関数を扱う手続き全てにコピーを探す何かを入れるのはだるい。(本質的にはadd-methodとremove-methodだけを特別視すればいいような気がしないでもないが、ちと自信がない。)

あまり使わない機能な上に直すとパフォーマンスに影響がでるから割と腰が重めである・・・

追記
add-methodがスレッドローカルなストレージにメソッドを格納してcompute-methodsがその辺をうまいこと何とかすればいけそうな気がしないでもない気がしてきた。 add-methodにオプション引数としてスレッドローカルか判別するフラグつけて、let-methodが呼び出すadd-methodはそのオプションを受け付けるようにしてやればよさそう。(もしくはadd-method-localを作るか。) remove-methodも同様にしてやる必要があるが、変に上記のようにごにょごにょするよりはすっきりしているかもしれない。

No comments:

Post a Comment