使用第二线程和TGIFImage导致GDI句柄泄漏

16

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

m_poBitmap32->Assign(poGIFImage);
即,刚打开的GIF图像被赋值给了一个由线程拥有的位图。所有这些都不与任何其他线程共享,即完全局限于该线程。它是时间相关的,因此并不是每次执行该行时都会发生,但当它发生时,它只发生在那一行上。每个泄漏都是一个DC、一个调色板和一个位图。(我使用比Process Explorer更详细的GDI信息工具 GDIView 。) 这里的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) 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.

     

[...]

     

但这个声明是完全错误的:即使其他线程不触摸它,您也必须在辅助线程中锁定画布。否则,画布的GDI句柄可以异步地在主线程中释放为未使用的状态。

另一个回答指出了类似的问题,这可能与graphics.pas中的GDI对象缓存有关。

这很可怕:在一个线程中创建并使用的对象可以在主线程中异步释放一些资源。遗憾的是,我不知道如何将锁定建议应用于TGIFImageTGIFImage没有Canvas,但它有一个具有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句柄。


4
"这很可怕:一个完全在一个线程中创建和使用的对象,可能会在主线程中异步释放一些资源" - 确实非常可怕。即使你知道这个明显的错误,你能做什么呢?即使有一种可行的锁定机制,GUI线程可能在你锁定之前释放某些资源 :((" - Martin James
2
@David - 经过确认,使用D2007。我无法解决这个错误,但我用'jvcl\devtools\res2bmp'文件夹中的'GIFImage.pas'替换了默认的'GIFImg.pas',测试应用程序仍在运行(大约10分钟)。不幸的是,差异太大了,无法进行比较,但我猜你可以找到问题所在,因为你知道该在哪里寻找。 - Sertac Akyuz
4
当TBitmap在第二个线程中使用时,可能会存在问题。线程并不像您想象的那样拥有它们。请检查Graphics单元,并阅读关于FreeMemoryContexts/BitmapCanvasList的注释。我曾经遇到过奇怪的随机异常,在Thread中使用TBitmap而不接触主VCL Thread(锁定位图画布或不锁定)......也许我不完全了解如何使用它。另外,您的TGifImage可能正在使用另一个独立的线程来处理帧...我个人完全弄不清楚,放弃了。 - kobik
4
顺便说一下,我的结论也是“即使其他线程不涉及它,你必须在次要线程中锁定画布”,因为VCL主winproc将释放未锁定的内存DC,从而破坏位图DC! - kobik
1
@FreeAndNil 当时不行,但在2017年我会尝试使用Windows图像组件(WIC)支持GIF,看看效果如何。Delphi也支持这一功能。 - David
显示剩余13条评论
1个回答

1

很不幸,修复方法非常丑陋。基本想法是后台线程必须在主线程在消息之间时获取一个锁。

简单的实现方式如下:

  1. 锁定画布互斥体。
  2. 创建后台线程。
  3. 等待消息。
  4. 释放画布互斥体。
  5. 处理消息。
  6. 锁定画布互斥体。
  7. 返回第3步。

请注意,这意味着后台线程只能在主线程忙碌时访问 GDI 对象,而不能在等待消息时访问它们。这也意味着后台线程在不持有互斥体时无法拥有任何画布。这两个要求通常会令人感到痛苦。因此,您可能需要完善算法。

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

我认为这已经足够让您放弃这个想法了。相反,也许应该在后台线程中读取文件,但在主线程中处理它。


真的是一个丑陋的修复!不过还是谢谢。目前,我已经放弃了对GIF图像的支持 - 我认为这比这更可取。我也认为这将非常难以实现,因为泄漏是由需要修改的VCL代码引起的。 - David

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接