【问题标题】:GDI handle leak using TGIFImage in a second thread在第二个线程中使用 TGIFImage 处理 GDI 处理泄漏
【发布时间】:2012-05-01 04:09:03
【问题描述】:

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

m_poBitmap32->Assign(poGIFImage);

也就是说,刚刚打开的 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)

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

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

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


更多详情

一些研究表明I'm not the only person to encounter this。引用一个线程,

帮助 (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

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

【问题讨论】:

  • '这很可怕:一个完全在一个线程中创建和使用的对象可以在主线程中异步释放它的一些资源'——确实,非常可怕。即使你知道这个明显的错误,你能做什么?即使有一个有效的锁定机制,GUI线程也可能在你锁定它之前释放一些东西:((
  • @David - 已通过 D2007 确认。我无法解决这个错误,但我用 'jvcl\devtools\res2bmp' 文件夹中的 'GIFImage.pas' 替换了库存 'GIFImg.pas' 并且测试应用程序仍在运行(大约 10 分钟)。不幸的是,差异有太多差异,但我想你可以弄清楚,因为你知道在哪里看。
  • 在第二个线程中使用 TBitmap 时存在(可能)问题。 The Thread 并不像你想象的那样拥有它们。检查Graphics 单元,并阅读有关FreeMemoryContexts/BitmapCanvasList 的注释。我有奇怪的随机异常,线程中使用的 TBitmaps 没有触及主 VCL 线程(是否锁定位图画布)......也许我不完全理解如何使用它。此外,您的 TGifImage 可能正在使用另一个单独的线程来处理帧......我个人对此完全迷失了,并放弃了它。
  • 顺便说一句,我的结论也是“您必须将画布锁定在辅助线程中,即使其他线程没有触及它。”因为VCL主winproc会释放未锁定的内存DC,杀死位图DC!
  • Graphics.pas: '(垃圾收集)。只有未被其他线程锁定的内存 DC 才会被释放。 GC!被Java入侵!

标签: delphi thread-safety gdi c++builder memory-leaks


【解决方案1】:

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

天真的实现是这样的:

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

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

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

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

【讨论】:

  • 真是一个丑陋的修复!不过谢谢。目前,我只是简单地放弃了对 GIF 图像的支持——我认为这比这更可取。我也认为这将很难实现,因为泄漏是由需要修改的 VCL 代码引起的。
猜你喜欢
  • 2011-12-23
  • 1970-01-01
  • 2018-12-18
  • 2011-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-01-08
  • 1970-01-01
相关资源
最近更新 更多