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



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

m_poBitmap32->Assign(poGIFImage);

也就是说,刚刚打开的 GIF 图像被分配给线程拥有的位图。 这些都不与任何其他线程共享,即完全本地化到线程。 它与时间相关,因此不会在每次执行该行时发生,但是当它确实发生时,它只发生在该行上。 每个泄漏是一个 DC、一个调色板和一个位图。(我使用 GDIView,它提供比进程资源管理器更详细的 GDI 信息。 m_poBitmap32 这是一个 Graphics32 TBitmap32 对象,但我使用纯 VCL 类(即使用 Graphics::TBitmap::Assign )重现了它。

最终我得到了一个EOutOfResources异常,可能表明桌面堆已满:

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

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

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

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


更多详情

一些研究表明,我不是唯一遇到这种情况的人。 引用一个线程,

帮助 (2007) 说: 在使用 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;
    }
}

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

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

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

朴素的实现是这样的:

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

请注意,这意味着后台线程只能在主线程繁忙时访问 GDI 对象,而不是在等待消息时访问。这意味着后台线程在不持有互斥锁的情况下不能拥有任何画布。这两个要求往往太痛苦了。因此,您可能需要优化算法。

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

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

最新更新