【问题标题】:Why does the following code using IOmniThreadPool cause access violations?为什么以下使用 IOmniThreadPool 的代码会导致访问冲突?
【发布时间】:2014-05-28 09:01:24
【问题描述】:

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

【问题讨论】:

  • 这看起来和另一个问题一样。你真的应该只问一个。我建议你删除其中一个。您能否进一步简化这一点。理想情况下,应该有一个没有 UI 的 .dpr 文件。一个控制台应用程序。
  • 感谢您的反馈!我现在删除了另一个问题并简化了示例代码。
  • 出色的工作。现在真的是超级SSCCE。如果允许,我会再次投票!

标签: delphi threadpool access-violation omnithreadlibrary


【解决方案1】:

这里有一个难以找到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;

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

【讨论】:

  • 非常感谢您的详细回复!实际上,我认为我以这种方式实现此功能的部分原因是因为我不确定如果在任务完成后调用 TOmniTaskControl.Comm.Receive 会发生什么(在原始应用程序中 MSG_CALLEE_FINISHED 附加了数据) .但似乎在 TOmniTaskControl.WaitFor 之后调用 TOmniTaskControl.Comm.Receive 是完全没有问题的 :) 我现在已经在我们的应用程序中以这种方式实现了它。再次感谢您的帮助。
  • 只要任务控制器处于活动状态并且消息正在等待您阅读它们,通信通道就会保持活动状态。
【解决方案2】:

您的终止消息似乎导致了问题。删除消息和 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 经常是未分配的。

【讨论】:

  • 非常感谢您的详细回复!根据我到目前为止所做的测试,您添加 .OnTerminated(procedure begin end) 的建议似乎解决了我们应用程序中的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-30
  • 2019-12-29
  • 2016-07-10
  • 2020-12-28
  • 1970-01-01
相关资源
最近更新 更多