我有一个后台线程,用于加载图像(从磁盘或服务器),目标是最终将它们传递到主线程进行绘制。 当第二个线程使用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对象缓存有关。
这很可怕:在一个线程中创建并使用的对象可以在主线程中异步释放一些资源。遗憾的是,我不知道如何将锁定建议应用于TGIFImage
。TGIFImage
没有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句柄。
Graphics
单元,并阅读关于FreeMemoryContexts
/BitmapCanvasList
的注释。我曾经遇到过奇怪的随机异常,在Thread中使用TBitmap而不接触主VCL Thread(锁定位图画布或不锁定)......也许我不完全了解如何使用它。另外,您的TGifImage可能正在使用另一个独立的线程来处理帧...我个人完全弄不清楚,放弃了。 - kobik