在我们的Delphi XE4应用程序中,我们使用MaxExecuting = 4的OmniThreadPool来提高某个计算的效率。不幸的是,我们遇到了间歇性访问冲突的问题(例如参见下面的MadExcept错误报告http://ec2-72-44-42-247.compute-1.amazonaws.com/BugReport.txt)。我能够构建以下示例来演示该问题。运行以下控制台应用程序后,System.SyncObjs.TCriticalSection.Acquire中的访问冲突通常会在一分钟左右内发生。有人能告诉我在下面的代码中我做错了什么,或者告诉我另一种方法来达到预期的结果吗?
program OmniPoolCrashTest;
{$APPTYPE CONSOLE}
uses
Winapi.Windows, System.SysUtils,
DSiWin32, GpLists,
OtlSync, OtlThreadPool, OtlTaskControl, OtlComm, OtlTask;
const
cTimeToWaitForException = 10 * 60 * 1000; // program exits if no exception after 10 minutes
MSG_CALLEE_FINISHED = 113; // our custom Omni message ID
cMaxAllowedParallelCallees = 4; // enforced via thread pool
cCalleeDuration = 10; // 10 miliseconds
cCallerRepetitionInterval = 200; // 200 milliseconds
cDefaultNumberOfCallers = 10; // 10 callers each issuing 1 call every 200 milliseconds
var
gv_OmniThreadPool : IOmniThreadPool;
procedure OmniTaskProcedure_Callee(const task: IOmniTask);
begin
Sleep(cCalleeDuration);
task.Comm.Send(MSG_CALLEE_FINISHED);
end;
procedure PerformThreadPoolTest();
var
OmniTaskControl : IOmniTaskControl;
begin
OmniTaskControl := CreateTask(OmniTaskProcedure_Callee).Schedule(gv_OmniThreadPool);
WaitForSingleObject(OmniTaskControl.Comm.NewMessageEvent, INFINITE);
end;
procedure OmniTaskProcedure_Caller(const task: IOmniTask);
begin
while not task.Terminated do begin
PerformThreadPoolTest();
Sleep(cCallerRepetitionInterval);
end;
end;
var
CallerTasks : TGpInterfaceList<IOmniTaskControl>;
i : integer;
begin
gv_OmniThreadPool := CreateThreadPool('CalleeThreadPool');
gv_OmniThreadPool.MaxExecuting := cMaxAllowedParallelCallees;
CallerTasks := TGpInterfaceList<IOmniTaskControl>.Create();
for i := 1 to StrToIntDef(ParamStr(1), cDefaultNumberOfCallers) do begin
CallerTasks.Add( CreateTask(OmniTaskProcedure_Caller).Run() );
end;
Sleep(cTimeToWaitForException);
for i := 0 to CallerTasks.Count-1 do begin
CallerTasks[i].Terminate();
end;
CallerTasks.Free();
end.
答案 0 :(得分:7)
这里有一个难以发现Task controller needs an owner问题的例子。发生的事情是任务控制器有时会在任务本身之前被销毁,并导致任务访问包含随机数据的内存。
问题场景如下([T]标记任务,[C]标记任务控制器):
在Graymatter的解决方法中,OnTerminated
为OmniThreadLibrary中的任务创建了一个隐式所有者,该任务“解决”了这个问题。
等待任务完成的正确方法是调用taskControler.WaitFor。
procedure OmniTaskProcedure_Callee(const task: IOmniTask);
begin
Sleep(cCalleeDuration);
end;
procedure PerformThreadPoolTest();
var
OmniTaskControl : IOmniTaskControl;
begin
OmniTaskControl := CreateTask(OmniTaskProcedure_Callee).Schedule(gv_OmniThreadPool);
OmniTaskControl.WaitFor(INFINITE);
end;
我将研究用引用计数的解决方案替换共享内存记录,这样可以防止出现这些问题(或至少使它们更易于查找)。
答案 1 :(得分:5)
看起来您的终止消息导致了问题。删除消息和WaitForSingleObject停止了AV。在我的测试中,只需在.Schedule之前添加.OnTerminated(过程开始结束)也足以改变流程并停止错误。所以这种情况下的代码看起来像这样:
procedure PerformThreadPoolTest();
var
OmniTaskControl : IOmniTaskControl;
begin
OmniTaskControl := CreateTask(OmniTaskProcedure_Callee).OnTerminated(procedure begin end).Schedule(gv_OmniThreadPool);
WaitForSingleObject(OmniTaskControl.Comm.NewMessageEvent, INFINITE);
end;
在我看来,这可能是问题所在。 otSharedInfo_ref有一个名为MonitorLock的属性。这用于阻止对otSharedInfo_ref的更改。如果由于某种原因otSharedInfo_ref在获取等待时被释放,那么你可能会得到一些非常奇怪的行为
现有的代码如下所示:
procedure TOmniTask.InternalExecute(calledFromTerminate: boolean);
begin
...
// with internal monitoring this will not be processed if the task controller owner is also shutting down
sync := nil; // to remove the warning in the 'finally' clause below
otSharedInfo_ref.MonitorLock.Acquire;
try
sync := otSharedInfo_ref.MonitorLock.SyncObj;
if assigned(otSharedInfo_ref.Monitor) then
otSharedInfo_ref.Monitor.Send(COmniTaskMsg_Terminated,
integer(Int64Rec(UniqueID).Lo), integer(Int64Rec(UniqueID).Hi));
otSharedInfo_ref := nil;
finally sync.Release; end;
...
end; { TOmniTask.InternalExecute }
如果otSharedInfo_ref.MonitorLock.Acquire正忙着等待并且otSharedInfo_ref后面的对象被释放,那么我们最终会处于一个非常讨厌的地方。将代码更改为此会停止在InternalExecute中发生的AV:
procedure TOmniTask.InternalExecute(calledFromTerminate: boolean);
var
...
monitorLock: TOmniCS;
...
begin
...
// with internal monitoring this will not be processed if the task controller owner is also shutting down
sync := nil; // to remove the warning in the 'finally' clause below
monitorLock := otSharedInfo_ref.MonitorLock;
monitorLock.Acquire;
try
sync := monitorLock.SyncObj;
if assigned(otSharedInfo_ref) and assigned(otSharedInfo_ref.Monitor) then
otSharedInfo_ref.Monitor.Send(COmniTaskMsg_Terminated,
integer(Int64Rec(UniqueID).Lo), integer(Int64Rec(UniqueID).Hi));
otSharedInfo_ref := nil;
finally sync.Release; end;
...
end; { TOmniTask.InternalExecute }
我确实开始在OmniTaskProcedure_Callee方法中获取AV,然后在“task.Comm.Send(MSG_CALLEE_FINISHED)”行中,所以它仍然没有修复,但这应该有助于其他人/ Primoz进一步确定发生了什么。在新的错误中,task.Comm通常是未分配的。