16

画像を(ディスクまたはサーバーから)ロードするバックグラウンドスレッドがあり、最終的にそれらをメインスレッドに渡して描画することを目標としています。この 2 番目のスレッドが VCL のTGIFImageクラスを使用して GIF 画像をロードしている場合、このプログラムは、次の行がスレッドで実行されるたびにいくつかのハンドルをリークすることがあります。

m_poBitmap32->Assign(poGIFImage);

つまり、開いたばかりの GIF イメージは、スレッドが所有するビットマップに割り当てられています。これらはいずれも他のスレッドと共有されません。つまり、スレッドに完全にローカライズされます。これはタイミングに依存するため、行が実行されるたびに発生するわけではありませんが、発生するとその行でのみ発生します。各リークは、1 つの DC、1 つのパレット、および 1 つのビットマップです。( Process Explorer よりも詳細な GDI 情報を提供 するGDIViewm_poBitmap32を使用します。)ここにGraphics32 TBitmap32オブジェクトを示しますが、プレーンな VCL 専用クラスを使用して、つまり を使用してこれを再現しGraphics::TBitmap::Assignました。

最終的にEOutOfResources、おそらくデスクトップ ヒープがいっぱいであることを示す例外が発生します。

:7671b9bc KERNELBASE.RaiseException + 0x58
:40837f2f ; C:\Windows\SysWOW64\vclimg140.bpl
:40837f68 ; C:\Windows\SysWOW64\vclimg140.bpl
:4084459f ; C:\Windows\SysWOW64\vclimg140.bpl
:4084441a vclimg140.@Gifimg@TGIFFrame@Draw$qqrp16Graphics@TCanvasrx11Types@TRectoo + 0x4a
:408495e2 ; C:\Windows\SysWOW64\vclimg140.bpl
:50065465 rtl140.@Classes@TPersistent@Assign$qqrp19Classes@TPersistent + 0x9
:00401C0E TLoadingThread::Execute(this=:00A44970)

TGIFImageこれを解決し、バックグラウンド スレッドで安全に使用するにはどうすればよいですか?

次に、PNG、JPEG、または BMP クラスでも同じ問題が発生しますか? 私はこれまでのところ行っていませんが、それがスレッド化/タイミングの問題であることを考えると、TGIFImage.

C++ Builder 2010 (RAD Studio の一部) を使用しています。


詳細

いくつかの調査によると、これに遭遇したのは私だけではありません。あるスレッドから引用すると、

Help (2007) によると: Lock を使用してキャンバスを保護するマルチスレッド アプリケーションでは、キャンバスを使用するすべての呼び出しを Lock の呼び出しで保護する必要があります。使用前にキャンバスをロックしないスレッドは、潜在的なバグをもたらします。

[...]

ただし、このステートメントは絶対に誤りです。他のスレッドがキャンバスに触れていなくても、キャンバスをセカンダリ スレッドでロックする必要があります。それ以外の場合、キャンバスの GDI ハンドルは、メイン スレッドで未使用としていつでも (非同期に) 解放できます。

別の回答は、graphics.pas の GDI オブジェクト キャッシュに関係している可能性があることを示しています。

これは恐ろしいことです。1 つのスレッドで完全に作成および使用されるオブジェクトは、そのリソースの一部をメイン スレッドで非同期に解放することができます。残念ながら、Lock アドバイスを に適用する方法がわかりませんTGIFImage TGIFImageにはありませんCanvasBitmap、キャンバスを持つ があります。効果のないロック。TGIFFrame問題は実際には内部クラスであると思われます。また、TBitmap32 リソースをロックする必要があるかどうか、またはロックする方法もわかりません。TMemoryBackendGDIの使用を回避するビットマップにaを割り当てようとしましたが、効果はありませんでした。

再生

これは非常に簡単に再現できます。新しい VCL アプリを作成し、スレッドを含む新しいユニットを作成します。スレッドの Execute メソッドに、次のコードを配置します。

while (!Terminated) {
    TGraphic* poGraphic = new TGIFImage();
    TBitmap32* poBMP32 = new TBitmap32();
    __try {
        poGraphic->LoadFromFile(L"test.gif");
        poBMP32->Assign(poGraphic);
    } __finally {
        delete poBMP32;
        delete poGraphic;
    }
}

Graphics::TBitmapGraphics32 がインストールされていない場合に使用できます。

アプリのメイン フォームに、スレッドを作成して開始するボタンを追加します。上記と同様のコードを実行する別のボタンを追加します (1 回だけ、ループする必要はありません。私の場合は、TBitmap32 を作成する代わりにメンバー変数として格納し、最終的にフォームにペイントするように無効にします)。プログラムを実行します。ボタンをクリックしてスレッドを開始します。おそらく、GDI オブジェクトが既にリークしているのを目にするでしょうが、メイン スレッドで同様のコードを 1 回実行する 2 番目のボタンを押さないと (1 回で十分です。何かをトリガーしているように見えます)、リークが発生します。メモリ使用量が増加し、GDI ハンドルが毎秒数十の割合でリークすることがわかります。

4

1 に答える 1

1

残念ながら、修正は非常に醜いです。基本的な考え方は、メッセージ間でメイン スレッドが保持するロックをバックグラウンド スレッドが取得する必要があるということです。

単純な実装は次のようになります。

  1. キャンバス ミューテックスをロックします。
  2. バックグラウンド スレッドを生成します。
  3. メッセージを待ちます。
  4. キャンバス ミューテックスを解放します。
  5. メッセージを処理します。
  6. キャンバス ミューテックスをロックします。
  7. 手順 3 に進みます。

これは、バックグラウンド スレッドが GDI オブジェクトにアクセスできるのは、メイン スレッドがメッセージを待機している間ではなく、ビジー状態のときだけであることを意味することに注意してください。これは、バックグラウンド スレッドがミューテックスを保持していない間は、キャンバスを所有できないことを意味します。これらの 2 つの要件は、負担が大きすぎる傾向があります。したがって、アルゴリズムを改良する必要があるかもしれません。

改良の 1 つは、バックグラウンド スレッドがキャンバスを使用する必要があるときにメイン スレッドにメッセージを送信するようにすることです。これにより、メイン スレッドがキャンバス ミューテックスをより迅速に解放し、バックグラウンド スレッドがそれを取得できるようになります。

この考えをあきらめさせるには、これで十分だと思います。代わりに、おそらく、バックグラウンド スレッドからファイルを読み取り、メイン スレッドで処理します。

于 2012-06-09T06:57:44.873 に答える