ガベージコレクションを開始する際、CLR はまず、全てのスレッドを一時停止します。そして、それぞれのスレッドが実行中だったコードを確認し、「セーフポイント」に到達しているかどうかを調べます。
セーフポイントに到達しているスレッドは、ガベージコレクションが完了するまでそのまま一時停止にしておいて問題ありません。
セーフポイントに到達していないスレッドはそのまま一時停止にしておくわけにはいきません。しかし、ガベージコレクションの実行中は全てのスレッドを一時停止 しておく必要があります。そのため CLR は、スレッドの乗っ取りを行おうとします。簡単に言うと、現在実行中のメソッドの戻り先を書き換えてスレッドを再開させます。これにより、メソッドの完了後に特殊な関数を実行するようにし、この特殊な関数内でスレッドを一時停止するわけです。
ただし、乗っ取りを行おうとしてから 250 ミリ秒経過しても乗っ取れなかった (=メソッドが未完了) 場合、CLR は再びスレッドを一時停止し、再びセーフポイントに到達しているかどうかを調べます。到達している場合は、そのまま一時停止します。到達していない場合はスレッドの再開となりますが、前回乗っ取りを行おうとした時とは違うメソッドを実行していた場合は、再開前にそのメソッドの戻り先を書き換えます。再開後数ミリ秒待機しても乗っ取れなかった場合は、再びこの動作を繰り返します。
セーフポイントはコストが高いため、必要最低限の箇所にしか用意されません。具体的には、メソッド呼び出しを含まないループ構造にのみ用意されます。メソッド呼び出しを含まないループ構造では、いつまで経っても乗っ取りが出来ない可能性があるためです。
と、ここまでが前置きです。
これらを踏まえると、オブジェクトの型を破壊的に変換 のメソッドは、アンマネージポインタを取得してから型ハンドルの書き換えを行うまでの間にメソッド呼び出しを行わないようにすれば、ガベージコレクションによって問題を引き起こすことがなくなると思わます。
アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間のメソッド呼び出しは、Type.TypeHandle プロパティの呼び出しと、RuntimeTypeHandle.Value プロパティの呼び出しのみです。つまり、newType 引数から型ハンドルを取得する処理をアンマネージポインタ取得処理より前で行うように修正すれば良いわけです。
/// <summary>
/// 指定したオブジェクトを強制的に別の型に変換します。
/// このメソッドはオブジェクトの型を破壊的に変換するため、予期しない動作を引き起こす可能性があります。
/// </summary>
/// <param name="target">対象オブジェクト。</param>
/// <param name="newType">新しい型。</param>
static unsafe void ConvertType(object target, Type newType)
{
// 型ハンドルを取得
IntPtr newTypeHandle = newType.TypeHandle.Value;
// 対象オブジェクトに対する GC ハンドルを生成
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
// GC ハンドルの実際のエントリへのポインタを取得
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
// エントリにはオブジェクトへの参照が格納されている
void* targetPointer = *((void**)entryPointer);
// オブジェクトへのポインタは、オブジェクトヘッダーの型ハンドル部分を参照している
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
// 型ハンドルを書き換えることで型を強制的に変換する
*typeHandlePointer = newTypeHandle;
}
finally
{
// GC ハンドルの解放
targetGCHandle.Free();
}
}
これで、アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間に、ガベージコレクションによってオブジェクトが移動することはなくなったはずです。
次の検証用コードで検証した結果、問題ありませんでした。
static void Main(string[] args)
{
bool run = true;
Action action =
delegate
{
while (run)
{
object o = new object();
Console.WriteLine("collect.");
GC.Collect();
}
};
action.BeginInvoke(null, null);
while (true)
{
object o = new object();
ConvertType(o, typeof(void));
Console.WriteLine(o.GetType());
if (o.GetType() != typeof(void))
{
Console.WriteLine("failed.");
run = false;
break;
}
}
Console.ReadLine();
}
ちなみに、修正前のメソッドで検証してみても、問題は中々発生しません。簡単に発生させるためには、オブジェクトの型ハンドルを書き換えている箇所にブレークポイントを貼ってデバック実行します。ブレークと再開を何回か繰り返せば簡単に発生します。
もちろん、修正後のメソッドならこの方法で検証しても問題が発生しません。