LoginSignup
32
38

More than 5 years have passed since last update.

やりがちなアンマネージドメモリのリークと対処方法

Last updated at Posted at 2015-12-12

はじめに

これまで何度か、AllocCoTaskMemを利用したUnmanagedリソースを利用したプログラムの記事を記載してきました。

.NETによる画像処理の高速化Tips:unsafe編(改稿:2015/11/08)
AllocHGlobalとAllocCoTaskMem どちらを使うべきか?

具体的には以下のようなコードを利用してきました。

IntPtr srcIntPtr = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(length);
try
{
    // ここに処理を記載する
}
finally
{
    System.Runtime.InteropServices.Marshal.FreeCoTaskMem(srcIntPtr);
}

一見問題ないように見えますが、上記のコードは場合によって、リソースのリークをもたらします。
この問題は別にAllocCoTaskMemの利用に限らず、たとえばP/Invokeを利用するようなケースでも発生しうるため注意が必要です。
今回は、その問題点と対応策を記載します。

問題点

さて上記のコードですが、多くの場合は正常に動作します。
しかし場合によってリソースのリークが発生するケースがありえます。
もっともありがちなのは、上記のコードがバックグラウンドに起動されたThread上で動作しており、処理中にThreadのAbortメソッドを呼び出されるケースです。

AllocCoTaskMemが呼び出されてから、tryブロックに入る前にAbortが呼び出され、ThreadAbortExceptionが発生すると、finally句が実行されませんので、確保したメモリの開放が実施されず、リソースのリークが発生します。

では、以下のようなコードならば問題ないのでしょうか?

IntPtr srcIntPtr = IntPtr.Zero;
try
{
    srcIntPtr = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(length);
    // ここに処理を記載する
}
finally
{
    if(srcIntPtr != IntPtr.Zero)
    {
        System.Runtime.InteropServices.Marshal.FreeCoTaskMem(srcIntPtr);
    }
}

実はAllocCoTaskMemの呼び出しと、その呼び出し結果をsrcIntPtrに設定する処理はAtomicityを保障してくれないため、AllocCoTaskMemの呼び出しの完了後かつ、srcIntPtrへの代入前にThreadAbortExceptionが発生することは十分にありえます。

ではどのように対応すれば良いのでしょうか?
次節でその対処法を説明します。

制約された実行領域 (CER: Constrained Execution Region)の利用

上記のようなケースは、CERを利用することで対処が可能です。
CERの詳細な説明は以下を参照してください。

https://msdn.microsoft.com/ja-jp/library/ms228973.aspx

今回のケースであれば一番簡単な方法はRuntimeHelpersクラスのPrepareConstrainedRegionsメソッドを利用する方法でしょう。
具体的なコードは以下のとおりです。

System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
try { }
finally
{
    IntPtr srcIntPtr = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(length);
    try
    {
        // ここに処理を記載する
    }
    finally
    {
        System.Runtime.InteropServices.Marshal.FreeCoTaskMem(srcIntPtr);
    }
}

RuntimeHelpersクラスのPrepareConstrainedRegionsメソッドを呼び出すと、CERが準備されます。
もう少し具体的にいうと、RuntimeHelpersクラスのPrepareConstrainedRegionsメソッドの呼び出し直後にはtry句を記載する必要があり、そのtryに対応するcatch句とfinally句内部の処理は割り込まれないことが保障されます。
try句は普通に割り込まれる可能性がある点に注意が必要です。

ただ、本来であればCERはできるだけ短時間で開放するほうが好ましいでしょう。
上記のコードではメモリ確保後の処理はCERの外に出すべきです。
具体的には以下のような記載が好ましいと思います。

IntPtr srcIntPtr = IntPtr.Zero;
try
{
    System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions();
    try { }
    finally
    {
        srcIntPtr = System.Runtime.InteropServices.Marshal.AllocCoTaskMem(length);
    }
    // ここに処理を記載する
}
finally
{
    if(srcIntPtr != IntPtr.Zero)
    {
        System.Runtime.InteropServices.Marshal.FreeCoTaskMem(srcIntPtr);
    }
}

最後に

今回はRuntimeHelpersクラスのPrepareConstrainedRegionsメソッドを利用したCERの使用方法を記載しました。
CERの利用は他にもReliabilityContractを利用する方法もあります。
ReliabilityContractの利用方法や、今回の記事の内容の詳細も以下に記載がありますので、実際に利用する際には一通り目を通されることをお勧めします。

https://msdn.microsoft.com/ja-jp/magazine/dd819080.aspx?f=255&MSPPError=-2147217396

また、実際の画像処理の場合などは、本記事のように確保したメモリに対して1メソッド内で処理を完結させて開放することができる場合ばかりではないでしょう。
その場合は、SafeHandleの併用を検討する必要があります。
SafeHandleについてはまた機会があれば記事にしたいと思います。

以上です。
最後までお付き合いいただき、ありがとうございました!

32
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
38