Gauche:MTと文字列

Gauche:MTと文字列

0.8.5までの文字列を扱うC APIはthread safeではない、という話。 発端はyarv-dev:636あたり。

問題

Schemeの文字列は文字単位で破壊的変更が可能。Gaucheの場合 マルチバイト文字列を使っているので、一文字の置き換えが文字列の size (バイト数で計った長さ) を変える可能性がある。

0.8.5まではScmStringにsizeとstart (文字列本体のバイト列へのポインタ) を 保持していて、破壊的変更の際には新たにアロケートしたバイト列をstartに セットしてsizeを変更していた。しかしこれと同時に別スレッドがsizeとstart を読もうとしているとrace conditionが発生する。一時的にバイト列の実長 よりもsizeの示す大きさの方が大きいと、不正なメモリ参照が生じる危険がある。

Gaucheの基本方針は、マルチスレッドでのオブジェクトアクセスの 排他制御はSchemeプログラム側の責任としているが、文字列の変更は Scheme側から見たらアトミックな操作であるはずだし、 C側でもSEGVるような状態をもたらすのはさすがにまずい。 かといって文字列アクセスの度にmutexでロックするのは重すぎる。

0.8.6での方針

構造

一番綺麗な解決法は文字列をimmutableにしちゃうことなんだけど、 R5RS compliantじゃなくなっちゃうんで困る。

そこで、一段immutableの構造を介することにした。

ScmString (mutable)
  SCM_HEADER
  body     -------> ScmStringBody (immutable)
                      length
                      size
                      flags          +---------------+
                      start -------->|byte array ... |(immutable)
                                     +---------------+

文字列を変更するプリミティブは、新たにScmStringBodyとbyte arrayを アロケートして、ScmStringのbodyポインタをすげ変える。

文字列を参照するプリミティブは、最初にScmStringBodyへのポインタを 掴んで、そこから各フィールドへアクセスするものとする。そしたら その最中に文字列が変更されても不正なアクセスを引き起こすことはない。

ただ、この方法にはデメリットもある。

文字列の変更にかかるコストが高いのは織り込み済なので問題にしない。 もともとbyte arrayのreallocationは必要だったわけだから。 Gaucheプログラマは、string-set!やstring-fill!は互換性のためだけ だと考えるべし。

で、文字列の変更がrareであって、ほとんどの文字列が作成されてから 一度も変更されずにその生涯を終えるのだとすれば、最初のScmStringBodyを ScmString中に取り込んでしまっても良いではないか。

typedef struct ScmStringRec {
  SCM_HEADER
  const ScmStringBody *body
  ScmStringBody initialBody;
} ScmString;

bodyは最初NULLで、それはinitialBodyを使うことを意味する。 変更があった場合は新たにScmStringBodyをアロケートしてbodyがそちらを指す。 (bodyを最初はinitialBodyのアドレスを指すように初期化できれば アクセスが簡単になるんだが、Cのマクロだけを使ってstaticな文字列の定義時に そういう初期化を行うのは難しそうだ。特にstatic ScmStringの配列の初期値を 書く場合)。

ペナルティは、

たぶん問題になりそうなのは最後の項目で、巨大な文字列を変更したりすると メモリが無駄になる危険はある。これについては文字列の変更は悪である、という キャンペーンで対抗しよう。

なお、今回のケースに限っては、「変更時にsizeが小さくなる場合、 byte arrayは元のsizeでアロケートする」という方針でも安全ではある。 でもimmutable structureの方が後々問題が出なさそうなのでそちらを採用。

API

低レベルAPIとしては、

でScmStringBodyへのポインタを取り出し、

等で要素にアクセスしてもらう。

高レベルAPIとしては、従来のScm_GetString, Scm_GetStringConstはそのまま 使えるのに加えて、

これでアトミックにsize, length, flagsを取り出す。

互換性

一応、SCM_STRING_SIZE, SCM_STRING_LENGTH, SCM_STRING_START等の マクロは残す。MT-unsafeなので、deprecatedとし、徐々に新APIに以降してもらう。

マクロを介さず直接フィールドにアクセスしてるコードは知らない。


Last modified : 2005/10/09 11:10:54 UTC