为什么使用IOmniThreadPool的以下代码会导致访问冲突?

时间:2014-05-28 09:01:25

标签: delphi threadpool access-violation omnithreadlibrary

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

2 个答案:

答案 0 :(得分:7)

这里有一个难以发现Task controller needs an owner问题的例子。发生的事情是任务控制器有时会在任务本身之前被销毁,并导致任务访问包含随机数据的内存。

问题场景如下([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;

我将研究用引用计数的解决方案替换共享内存记录,这样可以防止出现这些问题(或至少使它们更易于查找)。

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