16

我有一个加载图像(从磁盘或服务器)的后台线程,目的是最终将它们传递给主线程进行绘制。当第二个线程使用 VCL 的TGIFImage加载 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 的一部分。)


更多细节

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

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

[...]

但是这种说法是绝对错误的:即使其他线程没有触及它,您也必须将画布锁定在辅助线程中。否则,画布的 GDI 句柄可以在任何时候(异步)在主线程中被释放为未使用。

另一个回复表示类似的情况,可能与 graphics.pas 中的 GDI 对象缓存有关。

这很可怕:完全在一个线程中创建和使用的对象可以在主线程中异步释放它的一些资源。不幸的是,我不知道如何将 Lock 建议应用于TGIFImage. TGIFImage没有Canvas,尽管它确实有 aBitmap有一个画布。锁定无效。我怀疑问题实际上出在TGIFFrame一个内部类中。我也不知道是否或如何锁定任何 TBitmap32 资源。我确实尝试将 a 分配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;
    }
}

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

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

4

1 回答 1

1

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

天真的实现是这样的:

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

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

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

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

于 2012-06-09T06:57:44.873 回答