delphi - 在第二个线程中使用 TGIFImage 的 GDI 句柄泄漏

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

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

m_poBitmap32->Assign(poGIFImage);

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

Help (2007) says: In multi-threaded applications that use Lock to protect a canvas, all calls that use the canvas must be protected by a call to Lock. Any thread that does not lock the canvas before using it will introduce potential bugs.

[...]

But this statement is absolute false: you MUST lock the canvas in secondary thread even if other threads don't touch it. Otherwise the canvas's GDI handle can be freed in main thread as unused at any moment (asynchronously).

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

这太可怕了:完全在一个线程中创建和使用的对象可以在主线程中异步释放其部分资源。不幸的是,我不知道如何将锁定建议应用于 TGIFImage 尽管 TGIFImage 没有 Canvas,但它确实有一个带有 Canvas 的位图。锁定没有任何效果。我怀疑问题实际上出在内部类 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 句柄。

最佳答案

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

简单的实现是这样的:

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

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

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

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

关于delphi - 在第二个线程中使用 TGIFImage 的 GDI 句柄泄漏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10231556/

相关文章:

Delphi:设置 TIdHttpServer 连接超时

windows - 如何在Windows服务上添加证书

ios - 通过 NSKeyedArchiver 归档数据时崩溃

c# - 线程安全的扩展方法?

c - 绘画代码在我的 WM_COMMAND 消息处理程序中无法正常工作

c# - 与 C#/WinForms 中经过深度优化的 GDI 代码相比,SharpDX 可以带来多少改进?

delphi - 如何在Delphi中调用TObjectDictionary的继承构造函数

sql-server - SQL Server : Why would i add ";1" to the end of a stored procedure name?

java - 在 spring 中设置和获取 session 绑定(bind)属性

c++ - StretchBlt 收缩损坏