GCのfinalizer中からSchemeコードを呼びたい、という議論。
当初は下の「元の議論」にあるように「困難」という結論だったのだけれども、 どうしてもfinalizerからSchemeコードが呼ばれるケースが出てきてしまった。 具体的には、virtual portのサポートを追加したら、GCされるポートから Schemeで定義したflushメソッドやcloseメソッドが呼ばれる可能性が出てきたからだ。
で、改めてBoehm GCのコードを読んでみた。いくつかgood news。
さて、これだけでfinalizerからSchemeコードを走らせることは可能かというと、 唯一微妙なのは、VMの状態かな。Schemeコードが走ると、VM中の スタックフレームを移動する場合がある。その時点で Cのスタックフレームの方からVMのスタックフレームへの参照が存在すると まずい。今はVMループ中でScm_ConsとかはVMレジスタの退避無しで 呼んでいるので、こいつをレジスタ退避で保護するのと、Cのfinalizerで フラグを立てておいてVMループの先頭でフラグチェックしてScheme finalizerを 走らせるのとどっちが速いだろうか。
どうせシグナルのチェックはやっているので、フラグをビットマスクでやれば チェックのコストは変わらないか。 だがフラグ方式だと、フラグを立てた後、VMループに戻る前に スレッドが待ちに入ると、キューイングされたfinalizerが実行されない 可能性はあるな。
akrさんからreferされてた。
一般的にfinalizerからできることは何か、という議論と、 finalizerからScheme処理系へ再入することの問題は分けて議論したほうが 良いのではと思った。例に出ているcryptの問題は、Cで書いてても起こる (Scheme等のレイヤが入ることで見えにくくなる可能性はあるにせよ)。 確かこのへんはHans Boehmが去年の論文でいろいろな落し穴を書いてたはず。 これだ。
基本的にfinalizerは(シングルスレッドアプリでも)別スレッドで 走るようなものなので、排他制御不可欠ってことなんだが、 完璧に透過的にやろうとすると、何にせよロックを握ったままアロケータを 呼ぶってことが不可能になる。 さすがにそれは縛りがきつすぎるので、言語によらず finalizerを書く側に気をつけてもらうしかない。
それとは別に、Schemeレイヤ特有の問題として、任意の時点からVMに再入 可能かって話がある。これはシグナル処理と同様、finalizerを呼ぶ必要がある というコールバックの中でフラグを立てておいて、VMループの安全な箇所で キューイングされていたfinalizerを呼ぶことで解決できる。 (以前はfinalize対象になったオブジェクトはsweepされちゃうので これができないと思っていたのだが、上記のようにGC_JAVA_FINALIZATIONを 使うと一回分延命されるので助かることがわかったのだ。)
(2004/11/23 04:42:58 PST) VMに自前のキューを持って置いて、BoehmのGCのfinalizer中で Schemeへのコールバックが必要になった場合にそれをenqueueして、 VM loopのsafe pointでチェックして実行する、というのを実装してみたんだが、 だめだった。というのは、enqueueの中でアロケータを呼ぶと、 そこで残りの(GCレベルの)finalizerが再び呼ばれる。その中で 再びSchemeへのコールバックが必要になると、enqueueへ再入してしまうのだ。 キュー長さを固定にすれば再入は避けられるが、一回に呼ばれるfinalizerの 数の上限は予測できない (バースト的に大量のfinalizerが呼ばれることがある) ので 固定長は無理だろう。
GCレベルで一回finalizerをキューイングしているので、それを利用させて もらうしかなさそう。
この方法の欠点
自前のキューを持つことに対する利点
(記録のために残す)
Shiro: これはそう簡単ではないんですよ。
まず、いくつかの前提:
(2)から、Schemeでのfinalizerを走らせるには、finalizerされそうになった オブジェクトをgc後まで保存しておいて、gcが終わった後で一気に走らせる 必要がありますが、それは(1)から不可能です。
また、(3)から、finalizeされるオブジェクトが保持しているポインタの 先のオブジェクトは既に回収されてしまっている可能性があります。 従ってそのオブジェクトをSchemeレベルに見せるわけにはいきません。 (これは、CでScm_RegisterFinalizerを使う場合も重要な注意点です。 気軽に使えるメカニズムではありません)。
Boehm GCはJavaのランタイムの実装にも使われてるので、 Javaのファイナライザのセマンティクスを実現する方法は無くはないと 思うのですが、Gaucheのランタイムは循環参照がありまくりなので 果してうまくできるかどうか。また、たとえ出来たとしても、 Schemeレベルで循環参照を明示的に断ち切らないといけなかったり すると、却って面倒になりそうです。
で、どんな場面でfinalizerが必要なんでしょうか。もし良ければ 別ページを作って議論しましょう。
ねるWiki:ねる: むむ。Guileにあるのを見つけたので簡単に考えていましたが、なかなか根深い問題があるのですね。
使いたい場面ですが、weak referenceを使ったコネクションプールを作るのに使いたかったのです。コネクションプールなので、以下のような動作をさせたいわけです。
コネクション(port)はgcされる際に自動的にcloseされるので主な動作には問題がないのですが、finalizerがないと「logに出力」機能が実現できずに、すこし不便だなぁと思ったものですからこのWishを書きました。
どなたか?: mark が終ったところで finalizer を呼び出せればいいんですかね. finalizer 中で memory allocation が起こったら例外発生って事にして. それができるぐらいなら世話は無いっていう意見もありそうですが. できたとしても finalizer のチェックと sweep で二回の scan が必要になる....
ねるWiki:ねる: ふとおもったのですが,ぼくの想定している使用方法ではgc対象objectの中身を触ってfinalizeしたいわけではなくて,ただ単にあるobjectがgcされましたよ,ってのにhookしたいだけないんですよね.gc中にどこかにキューイングしておいて後から適当なタイミングで呼び出すhookを設定できたら十分でしょうか.そのhookにはgc対象objectは渡さずに,hook設定時に別のobjectをわたしておくと.
こんな中途半端な機能は普通は必要とされないですかね...
Shiro(2004/09/17 01:44:11 PDT): いや、それいいかもしれません。finalizerが欲しい という要求の多くは、finalizeされるオブジェクトそのものよりも、 そのオブジェクトが握っているリソースに関心がある場合だと思います。 あるオブジェクトがfinalizeされた後で呼ばれるクロージャを登録 できるようになっていれば、オブジェクトそのものへの参照を持たずに、 必要なリソースの情報だけ握っておけば後処理が可能です。
考え得る問題点は、
(let ((x (make-some-object resource))) (add-object-gc-sentinel! x (lambda () (do-some-cleanup resource))))
使いこなすのが難しい機能になりそうですが、そもそも生のfinalizerの方が より使いこなすのが難しく、それに比べればずっと素直かもしれません。
ねるWiki:ねる: ああ、なるほど。たしかにうまくやればリソースの解放にも使えそうですね。自分で書けるかな...
Tag: GC