GDI在第二个线程中使用TGIFImage处理泄漏

Dav*_*vid 16 delphi memory-leaks gdi c++builder thread-safety

我有一个后台线程加载图像(从磁盘或服务器),目标是最终将它们传递给主线程进行绘制.当第二个线程使用VCL TGIFImage加载GIF图像时,每次在线程中执行以下行时,此程序有时会泄漏几个句柄:

m_poBitmap32->Assign(poGIFImage);
Run Code Online (Sandbox Code Playgroud)

也就是说,刚刚打开的GIF图像被分配给线程拥有的位图.这些都不与任何其他线程共享,即完全本地化到线程.它与时序有关,因此每次执行该行时都不会发生,但是当它确实发生时,它只发生在该行上.每个泄漏都是一个DC,一个调色板和一个位图.(我使用GDIView,它提供比Process Explorer更详细的GDI信息.) m_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)
Run Code Online (Sandbox Code Playgroud)

如何TGIFImage在后台线程中解决此问题并安全使用?

其次,我会遇到PNG,JPEG或BMP类的同样问题吗?我还没有到目前为止,但鉴于它是一个线程/时间问题并不意味着如果他们使用相似的代码我就不会TGIFImage.

我正在使用C++ Builder 2010(RAD Studio的一部分.)


更多细节

一些研究表明,我不是唯一遇到这种情况的人.引用一个帖子,

Help(2007)说:在使用Lock保护画布的多线程应用程序中,所有使用画布的调用都必须通过调用Lock来保护.在使用之前没有锁定画布的任何线程都会引入潜在的错误.

[...]

但是这个陈述是绝对错误的:即使其他线程没有触摸它,你也必须在辅助线程中锁定画布.否则,画布的GDI句柄可以在主线程中随时释放(异步).

另一个回复表明类似的东西,它可能与graphics.pas中的GDI对象缓存有关.

这很可怕:在一个线程中完全创建和使用的对象可以在主线程中异步释放一些资源.不幸的是,我不知道如何应用Lock建议TGIFImage. TGIFImage没有Canvas,虽然它确实有Bitmap一个画布.锁定无效.我怀疑这个问题实际上是TGIFFrame一个内部课程.我也不知道是否或如何锁定任何TBitmap32资源.我确实尝试将TMemoryBackend位图分配给位图,这避免了使用GDI,但它没有任何效果.

再生产

您可以非常轻松地重现这一点.创建一个新的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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Graphics::TBitmap如果您没有安装Graphics32,则可以使用.

在应用程序的主窗体中,添加一个创建并启动线程的按钮.添加另一个按钮,执行与上面相似的代码(只需一次,无需循环.我也将TBitmap32存储为成员变量而不是在那里创建它,并使其无效,最终将其绘制到表单中.)运行程序然后单击按钮以启动该线程.您可能会看到GDI对象已经泄漏,但如果没有按下第二个按钮,它在主线程中运行一次类似的代码 - 一次就足够了,它似乎会触发一些东西 - 它会泄漏.您将看到内存使用量上升,并且它以每秒几十个的速率泄漏GDI句柄.

Dav*_*rtz 1

不幸的是,修复方法非常非常难看。基本思想是,后台线程必须获取主线程在消息之间持有的锁。

天真的实现是这样的:

  1. 锁定画布互斥锁。
  2. 生成后台线程。
  3. 等消息。
  4. 释放画布互斥体。
  5. 处理消息。
  6. 锁定画布互斥锁。
  7. 转到步骤 3。

请注意,这意味着后台线程只能在主线程繁忙时访问 GDI 对象,而不能在等待消息时访问。这意味着后台线程在不持有互斥体时不能拥有任何画布。这两个要求往往太痛苦了。所以你可能需要改进算法。

一项改进是让后台线程在需要使用画布时向主线程发送消息。这将导致主线程更快地释放画布互斥体,以便后台线程可以获取它。

我想这足以让你放弃这个想法了。相反,也许从后台线程读取文件,但在主线程中处理它。