问题描述
我有一个简单的程序,它在一个单元的“初始化”中创建一个 OmniThread 工作池,并在该单元的“最终化”中销毁同一个池。只要我们不使用 EurekaLog,这就可以正常工作。如果我们包含 EurekaLog,则会在应用程序完成期间(关闭应用程序后)引发访问冲突。这种情况每 3 到 10 次程序结束才会发生一次;所以这似乎是某种时间问题。
似乎一切正常,如果我们在“正常”应用程序流中创建工作池(而不是在单独单元的初始化和完成中)。
本单元代码如下:
unit Unit1;
interface
uses
OtlParallel;
var
_Worker: IOmniBackgroundWorker;
implementation
initialization
_Worker := Parallel.BackgroundWorker.NumTasks(10)
.Execute(
procedure(const AWorkItem: IOmniWorkItem)
begin
//
end
);
finalization
_Worker.Terminate(INFINITE);
_Worker := nil;
end.
主应用也很简单:
uses
{$IFDEF EurekaLog}
EMemLeaks,EResLeaks,EDebugExports,EDebugJCL,EFixSafeCallException,EMapWin32,EAppVCL,EDialogWinAPimsClassic,EDialogWinAPIEurekaLogDetailed,EDialogWinAPIStepsToReproduce,ExceptionLog7,{$ENDIF EurekaLog}
Vcl.Forms,Unit1 in 'Unit1.pas';
{$R *.res}
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.Run;
end.
访问冲突的调用堆栈是:
:5653e4c4
OtlTaskControl.TOmniTask.Execute
OtlThreadPool.TOTPWorkerThread.ExecuteWorkItem($393A160)
OtlThreadPool.TOTPWorkerThread.Execute
System.Classes.ThreadProc($3976800)
EThreadsManager.NakedBeginThreadWrapper(???)
:76ee6359 KERNEL32.BaseThreadInitThunk + 0x19
:77628944 ntdll.RtlGetAppContainerNamedobjectPath + 0xe4
:77628914 ntdll.RtlGetAppContainerNamedobjectPath + 0xb4
我使用的是 OTL 和 EurekaLog 版本 7.9.1.4 update 1 hot-fix 4 的最新 master checkout。
我们创建和销毁工作池的方式是否正确?如果是这样,如果将它们一起使用,是否有人熟悉 OTL/EurekaLog 中的问题?
解决方法
这似乎是一个缺陷/未在 OTL 中实现:
destructor TOmniTaskControl.Destroy;
begin
// TODO 1 -oPrimoz Gabrijelcic : ! if we are being scheduled,the thread pool must be notified that we are dying !
看起来这段代码可以在线程池中调度一个任务,该任务仍在运行。
TOmniTaskControl
和 TOmniTask
共享同一个 TOmniSharedTaskInfo
实例。如果从 TOmniTaskControl
的终结中删除 OtlTaskControl
,则仍在运行的线程池中的任何 TOmniTask
将包含对已删除的 TOmniSharedTaskInfo
的引用。
在没有 EurekaLog 的应用程序中“不是问题”,因为删除对象的内存将保持不变。因此,访问已删除的对象将成功。但是,添加 EurekaLog 意味着使用调试模式擦除已处理的内存。因此,访问已删除的对象可能会失败。
具体来说,当 TOmniTask
尝试访问已删除的 TOmniSharedTaskInfo
时,代码会崩溃:
procedure TOmniTask.InternalExecute(calledFromTerminate: boolean);
// ...
begin
otCleanupLock.EnterWriteLock;
// ...
finally
otCleanupLock.EnterWriteLock;
if assigned(otSharedInfo_ref.ChainTo) and
(otSharedInfo_ref.ChainIgnoreErrors or (otExecutor_ref.ExitCode = EXIT_OK))
then
chainTo := otSharedInfo_ref.ChainTo; // - fails here inside System.@IntfCopy
otSharedInfo_ref.ChainTo := nil;
// ...
无法访问的 :5653e4c4
位置只是一个地址,System.@IntfCopy
在威胁来自 otSharedInfo_ref
的垃圾字段时尝试读取该地址。
您可以通过禁用 EurekaLog 中的“捕获内存问题”选项(在处理时覆盖对象/接口 VMT)并将“释放内存时”选项设置为“什么都不做”来确认这是一个“释放后使用”问题.如果你这样做 - 提到的访问已删除的 otSharedInfo_ref
/ TOmniSharedTaskInfo
内的 TOmniTask.InternalExecute
的崩溃将不再发生(确实 - 因为现在删除的 otSharedInfo_ref
将保持不变,因此将可以访问)。
此外,现在将发现新的内存泄漏:
OtlSync.pas TOmniCriticalSection.Create
OtlSync.pas CreateOmniCriticalSection
OtlSync.pas TOmniCS.Initialize
OtlTaskControl.pas TOmniCS.Acquire
OtlTaskControl.pas TOmniTask.InternalExecute
OtlTaskControl.pas TOmniTask.Execute
OtlThreadPool.pas TOTPWorkerThread.ExecuteWorkItem
OtlThreadPool.pas TOTPWorkerThread.Execute
System.Classes.pas ThreadProc
此泄漏发生在 otSharedInfo_ref.MonitorLock.Acquire
内的 TOmniTask.InternalExecute
。这发生在前面提到的 chainTo := otSharedInfo_ref.ChainTo
附近。
这只是对“释放后使用”错误的又一次确认。这是因为通常 ostiMonitorLock: TOmniCriticalSection
的 TOmniSharedTaskInfo
字段会在 otSharedInfo_ref: TOmniSharedTaskInfo
字段作为 TOmniTaskControl
销毁的一部分被删除时最终确定。
但是,如果存在“释放后使用”错误 - 那么 TOmniTaskControl
将被删除,而 TOmniTask
活着仍然持有对同一个 TOmniSharedTaskInfo
的引用。这意味着尝试 otSharedInfo_ref.MonitorLock.Acquire
将使用全新的临界区重新初始化 ostiMonitorLock: TOmniCriticalSection
。并且由于 TOmniTaskControl
及其 TOmniSharedTaskInfo
已经消失 - 将没有代码来清理这个新创建的临界区。实际上,您刚刚在已删除的对象 (TOmniSharedTaskInfo
) 中创建了一个新的临界区作为字段!