要約
- 参照を保持していても GC 対象になってしまうことがある
- マネージコード上で継続的に評価していないと Release ビルドの最適化により破棄される
- C++/CLI ラッパーフレームワークなどを運用していると起きがちなので気をつけよう
こんなケース
何がしかのグラフィクスフレームワークで、こんなアプリを作ったとします。
var viewer = new Viewer();
var shape = new Shape();
viewer.Register(shape);
while (viewer.Draw())
{
}
描画ループの持ち方はさておき、一見動いてくれそうに見えますが、これが GC 発動のタイミングでクラッシュすることがあります。
何故起きるのか
描画ループ内で shape が一度も評価されないことが原因です。Release ビルドにおいては、参照が保持されていても、長時間評価されないインスタンスが GC の対象になってしまうようです。
(追記)本来は Release ビルドの判断基準が正しく、Debug ビルドが~~気を利かせて?~~寿命を延長しているがために、挙動が異なっているようです。
viewer.Register() の際に参照を握っていれば良かったのですが、実はこのフレームワークは C++/CLI によるネイティブラッパーで、内部ではネイティブコード上での登録処理しか行っていませんでした。
延命実験
1. while ループ後に GC.KeepAlive() する
while (viewer.Draw())
{
}
GC.KeepAlive(shape);
アドホック感丸出しですが、その名の通り生存は担保されます。
(追記)描画ループ後に評価されることがマークされれば GC 対象から外れるので、ループ内で KeepAlive する必要はありませんでした。
2. 適当なラッパークラスを経由して適当に評価する
class ShapeWrapper
{
private int count;
public Shape Shape { get; set; }
public void Update() { ++count; }
}
やろうとしていることは適当に評価してごまかすことでしかないんですが、被評価判定が伝播するかどうかを確認してみます。
ちなみにカウンタのインクリメントをしているのは、ただの空関数呼び出しだと最適化の際になかったことにされるためです。小賢しい賢い。
var wrapper = new ShapeWrapper { Shape = shape };
while (viewer.Draw())
{
}
wrapper.Update();
これでも生存が確認できました。参照を保持しているクラスインスタンスの何かしらのメンバーが評価されていれば、破棄されることはないようです。
3. スコープを広げる
上記の実験により、何かしらのメンバが評価されていれば OK ということがわかりました。ならば、クラスのフィールドにしておけば、わざわざ生存を保障する必要もないのではないでしょうか。
class App
{
Viewer viewer = new Viewer();
Shape shape = new Shape();
void Run()
{
this.viewer.Register(this.shape);
while (this.viewer.Draw())
{
}
}
}
---
{
var app = new App();
app.Run();
}
コードにしてみれば一目瞭然ですが、フィールドを評価するには this を経由する必要がありますし、何よりメソッド呼び出し自体が this の評価の真っ最中ということになるので、各メンバーが勝手に破棄される恐れはなくなります。ローカル変数で長寿命のインスタンスを扱っていたこと自体が良くなかったのかもしれません。
ベストプラクティスは
色々検証はしてみたものの、そもそも利用者側にこんな気遣いをさせるようではフレームワークとして良い状態ではありません。ネイティブコード上での参照保持関係は、マネージコード上でも同じように保持するべきでした。そのようにしましょう。