10

在我们的 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.
4

2 回答 2

7

您在这里有一个难以找到的示例任务控制器需要所有者问题。发生的情况是任务控制器有时会在任务本身之前被破坏,这会导致任务访问包含随机数据的内存。

有问题的场景是这样的([T] 标记任务,[C] 标记任务控制器):

  • [T] 发送消息
  • [C] 收到消息并退出
  • [C] 被摧毁
  • 创建新任务 [T1] 和控制器 [C1]
  • [T] 试图退出;在此期间它访问由 [C] 管理但随后被属于 [C1] 或 [T1] 的数据破坏和覆盖的共享内存区域

在 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;

我将研究用引用计数解决方案替换共享内存记录,这将防止此类问题(或至少使它们更容易找到)。

于 2014-05-29T10:39:13.913 回答
5

看起来您的终止消息导致了问题。删除消息和 WaitForSingleObject 停止了 AV。在我的测试中,在 .Schedule 之前添加一个 .OnTerminated(procedure begin end) 也足以改变流程并停止错误。因此,这种情况下的代码如下所示:

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 通常是未分配的。

于 2014-05-28T22:34:48.037 回答