Gauche:ClassRedefinition

Gauche:ClassRedefinition

Class redefinition protocolの実装上の諸問題


クラス再定義プロトコル

STklos, Guileとも、(define-class name ...) が評価された時点で nameが <class>のインスタンスにバインドされていた場合、 新たなspecでクラスオブジェクトを作成した後、 クラス再定義プロトコルを起動する。CLOSのクラス再定義プロトコルとは異なる。

  class-redefinition (old <class>) (new <class>)          [gf]
    remove-class-accessors (old <class>)                          [gf]
    update-direct-method (method <method>) (old <class>) (new <class>) [gf]
    {update direct supers to point new from its direct subclass list}
    update-direct-subclass (class <class>) (old <class>) (new <class>) [gf]
    {mark old class to be redefined}

問題点

マルチスレッドへの対応

クラス再定義は副作用ありまくりの操作だが、 guile, stklosとも、 複数スレッドでクラスの再定義が起動された場合に対応していない。

クラスの再定義の効果はローカルではない。関連するメソッド、gf、 直接のスーパークラスと、サブクラス全てに影響を及ぼす。 考えなければならないケースはいくつかある。

  1. 複数のスレッドが同時にクラスの再定義を行った場合:
  2. あるスレッドがクラスの再定義を行っている最中に、 別スレッドがそのクラスのインスタンス (newのインスタンスはまだ作られていないから、oldのインスタンス) にアクセスした場合:
  3. あるスレッドが新しいインスタンスを構築している時に、 別のスレッドがそのクラスを再定義した場合:
  4. インスタンスアクセス中に別スレッドでクラス再定義が行われた場合:

束縛の変更

クラスxを再定義した場合、その環境でのxの束縛を旧クラスから新クラスへ 変更する必要がある。再定義するクラスそのものは、

  (define x (.... クラスを作って新クラスを返す ...))

というフォームへと展開されるから最終的に束縛が更新されることに なるんだが、問題は自動的に再定義されるサブクラスである。 CLOSと違って、クラスは原則無名であるから、クラスへのポインタから それが何に束縛されているかを知ることはできない。

guileの場合はサブクラスもidentityそのままに再定義されるから、 特に何もしないでも、サブクラスの束縛からは新しいサブクラスが 見えるようになる。 一方、stklos方式の場合はサブクラスの束縛を捜し出して再定義 してやらねばならない。(実はstklos-0.55はそれをやっていない。 ので、サブクラスの束縛は古いのを指したままになる)。

ad hocな解決法としては、クラスが最初に定義された環境を 持っておくしかない。束縛が別に移されていたら追えないが、 それは再定義されるクラスでも同じことなので。

これに関連するもうひとつの問題は、無名モジュールを作って 中でクラスを定義して、そのメソッドを有名モジュールにあるgeneric function に追加した場合。クラスが無名モジュールへのポインタを持っていると、 全ての構造がGCされなくなるのであんまり嬉しくない。 クラスから束縛モジュールへのポインタはweakにするべきだろう。 (これは後の課題とする)

ロールバック

再定義を巻戻すことはできるか。

guileもstklosもロールバックは考えていない。 最も問題となるのは、再定義中にエラーが発生した場合だ。 クラスの状態が中途半端になるので取り返しがつかない事態になる可能性がある。

再定義が無事済んでいる場合は、元の定義を使ってもう一度再定義してやれば クラスの論理的な振る舞いは元に戻せる。但し、インスタンスがupdateされて しまっていると、失われたスロットの情報は戻らない。

最初から全てをロールバック可能にしておく必要はないが、 MOPを使えばロールバック可能なクラスが作れるようにはしておく必要がある。

インスタンスアップデートにおけるinstance-allocated以外のスロットの扱い

stklosもguileも(そしてGaucheも)、クラスが再定義された時点では インスタンスはアップデートされず、古いクラス定義を指したままである。 slot-ref等のタイミングでインスタンスのクラス定義がチェックされ、 再定義されていた場合はchange-classメソッドを呼ぶことにより インスタンスアップデートプロトコルに入る。

インスタンスアップデートプロトコルは基本的には同じで、こんな感じ:

  change-class (old-instance, new-class)
    let new-instance = allocate(new-class)
    for each slot S of new-class:
      if (Sがold-classに存在し、束縛されている)
         new-instance.S = old-instance.S
      else
         slot-initialize(new-instance, S)
    new-instanceとold-instanceのidentityを入れ換え

注意すべきは、この中でold-instanceに対してslot-ref等をコールすると、 それが再びインスタンスアップデートプロトコルをトリガしてしまい、 無限ループに入ることだ。 そのため、old-instanceに対する全てのアクションには、 slot-ref-using-class等、インスタンスアップデートプロトコルを トリガしない低レベルアクセスメソッドが使われる。

問題は、old-instanceのスロットのアロケーションが:instance以外(例えば:virtual) であった場合だ。この場合、virtualスロットの:slot-refオプション内で、 インスタンスに対してslot-refを起動していることは十分にあり得るので、 そのスロットにアクセスに行ったら無限ループに入る。

stklosは0.55時点ではこの問題に対応しておらず、virtual slotのある クラスを再定義してインスタンスにアクセスするとSEGVる。

Guileは1.6.4時点では、Sのallocationがinstanceである場合に限り、 old-instanceのスロット内容をnew-instanceにコピーしている。 これは最も安全な方法だが、望ましくない作用もある。

とにかく、virtualスロット等、ユーザが与えた手続きに制御が渡ってしまうと 構造的な解析は不可能なので、捕まえるとしたら インスタンスアップデートプロトコルに対する再入を動的に検出するしかなさそうだ。

インスタンスアクセスとクラス再定義のMT hazard

スロットアクセスの流れ。slot-set!やslot-boundpもほぼ同様。

                               slot-ref(obj, slot)     slot-ref-using-class(class, obj, slot)
                                       |*                  |
   [accessor method](obj)              v                   v
             |*                  get-slot-accessor(class, slot)
             |                       |found            |not found
             |                       |                 |
             +-----------------------/                 v 
             |                                slot-missing(class, obj, slot) **
             v
      slot-ref-using-accessor(obj, sa) 
          |normal               |procedural
          |                     |
          v                     v
    [default slot-ref]     [scheme procedure](obj) **
       |       | unbound      |          | unbound
       v       |              v          |
     (value)   |            (value)      |
               |                         |
               \------------+------------/
                            |
                            v
                  slot-unbound(class, obj, slot) **

インスタンスのクラスは自分がchange-classを呼ばない限りは変わらない。 また、クラスの中身はたとえ再定義中であっても(redefinedスロットをのぞいて) 変化しないので、hazard無しでアクセスできる。

(複数のスレッドが同時にインスタンスにアクセスして同時にchange-classを トリガしたら…という問題は、そもそも複数のスレッドが同時アクセスし得る 時点でアプリレベルで排他制御をかけとくべきなんで、ここでは問題にしない)

基本的には、アクセスプロトコルに突入した時点でclassのredefinedスロットを チェックし、クラスが再定義されていたらchange-classを呼んでから プロトコルに再入する(上記*の箇所)。プロトコル内部ではchange-classを 呼んではならない(インスタンスの構造が変わるとまずい)。

やばげなのは、Schemeに処理が戻される**の部分である。 最初のチェック後、**に到るまでに再定義がなされた場合、 メソッドのspecializerは新しいクラスに変わっているのに インスタンスが古いクラスを指したままだと、メソッドが呼ばれないことになる。

メソッドディスパッチの際に、インスタンスのクラスを検査する時点で 再定義を検出してchange-classを呼んだら? それでも、メソッド検索の 期間中generic functionをロックしておかないと、完全ではない。 でもそれは重すぎるだろう。

別の方法として、メタクラスを使ってclass引数でディスパッチさせるという 手がある。クラスの再定義によってメタクラスが変わらない場合なら 同じメソッドを呼ばせることができる。当面これでゆこう。

Gaucheの再定義プロトコル

  redefine-class! old new [function]
    {lock old class}
      class-redefinition (old <class>) (new <class>)   [gf]
        update-direct-method! (m <method>) (old <class>) (new <class>) [gf]
        {update direct supers}
        update-direct-subclass (class <class>) (old <class>) (new <class>) [gf]
    {unlock old class and mark it redefined}

Last modified : 2003/11/27 07:01:54 UTC