如何停止正在运行的TTask线程安全?

use*_*674 5 delphi parallel-processing multithreading thread-safety wait

在柏林的Delphi 10.1中,我想添加一个可能性来停止我的问题中的响应式TParallel。&For循环。如何使TParallel。&For循环响应并将值存储在TList <T>中?

循环计算值并将这些值存储在TList中。它与TTask.Run在单独的线程中运行以使其响应:

type
  TCalculationProject=class(TObject)
  private
    Task: ITask;
    ...
  public
    List: TList<Real>;
    ...
  end;

procedure TCalculationProject.CancelButtonClicked;
begin
  if Assigned(Task) then
  begin
    Task.Cancel;
  end;
end;

function TCalculationProject.CalculateListItem(const AIndex: Integer): Real;
begin
  //a function which takes a lot of calculation time
  //however in this example we simulate the calculation time and
  //use a simple alogorithm to verify the list afterwards
  Sleep(30);
  Result:=10*AIndex;
end;

procedure TCalculationProject.CalculateList;
begin
  List.Clear;

  if Assigned(Task) then
  begin
    Task.Cancel;
  end;

  Task:=TTask.Run(
    procedure
    var
      LoopResult: TParallel.TLoopResult;
      Lock: TCriticalSection;
    begin
      Lock:=TCriticalSection.Create;
      try
        LoopResult:=TParallel.&For(0, 1000-1,
          procedure(AIndex: Integer; LoopState: TParallel.TLoopState)
          var
            Res: Real;
          begin

            if (Task.Status=TTaskStatus.Canceled) and not(LoopState.Stopped) then
            begin
              LoopState.Stop;
            end;
            if LoopState.Stopped then
            begin
              Exit;
            end;

            Res:=CalculateListItem(AIndex);
            Lock.Enter;
            try
              List.Add(Res);
            finally
              Lock.Leave;
            end;
          end
        );
      finally
        Lock.Free;
      end;

      if (Task.Status=TTaskStatus.Canceled) then
      begin
        TThread.Synchronize(TThread.Current,
          procedure
          begin
            List.Clear;
          end
        );
      end
      else
      begin
        if LoopResult.Completed then
        begin
          TThread.Synchronize(TThread.Current,
            procedure
            begin
              SortList;
              ShowList;
            end
          );
        end;
      end;
    end
  );
end;
Run Code Online (Sandbox Code Playgroud)

当前运行的计算任务应在以下情况下停止

  1. 计算重新开始
  2. 用户单击取消按钮

我加了

if Assigned(Task) then
begin
  Task.Cancel;
end;
Run Code Online (Sandbox Code Playgroud)

在单击“取消”按钮时的开始procedure TCalculationProject.CalculateListprocedure TCalculationProject.CancelButtonClicked调用中。

循环停止

if (Task.Status=TTaskStatus.Canceled) and not(LoopState.Stopped) then
begin
  LoopState.Stop;
end;
if LoopState.Stopped then
begin
  Exit;
end;
Run Code Online (Sandbox Code Playgroud)

并使用清除清单

if (Task.Status=TTaskStatus.Canceled) then
begin
  TThread.Synchronize(TThread.Current,
    procedure
    begin
      List.Clear;
    end
  );
end
Run Code Online (Sandbox Code Playgroud)

当我重新开始计算时,这不起作用。然后,两个计算任务正在运行。我尝试添加一个Task.Waitafter Task.Cancel来等待任务完成,然后再开始新的计算,但是没有成功。

实现这种取消/停止功能的正确的完全线程安全的正确方法是什么?

LU *_* RD 4

原因Wait不行,是死锁。该Synchronize调用Wait有效地阻止正在运行的任务完成。

如果将所有Synchronize调用更改为Queue,您将避免死锁。但是,与正在运行的任务Task.Cancel一起调用会引发错误,因此那里没有运气。Task.WaitEOperationCancelled


更新:这被报告为一个错误,并且在 Delphi 10.2.3 Tokyo 中仍未修复。https://quality.embarcadero.com/browse/RSP-11267


为了解决这个特定问题,您需要在Task结束时收到通知,无论是完成、取消还是停止。

当任务启动时,UI 应阻止任何启动新计算的尝试,直到前者准备好/取消。

  • 首先,当计算任务开始时,禁用开始新计算的按钮。
  • 其次,同步或排队调用以在任务结束时启用按钮。

现在,有一种安全的方法可以知道任务何时完成/停止或取消。完成后,删除方法if Assigned(Task) then Task.Cancel中的语句CalculateList


如果该CalculateListItem方法很耗时,请考虑定期检查其中的取消状态标志。


一个例子:

type
  TCalculationProject = class(TObject)
  private
    Task: ITask;
  public
    List: TList<Real>;
    procedure CancelButtonClicked;
    function CalculateListItem(const AIndex: Integer): Real;
    procedure CalculateList(NotifyCompleted: TNotifyEvent);
    Destructor Destroy; Override;    
  end;

procedure TCalculationProject.CancelButtonClicked;
begin
  if Assigned(Task) then
  begin
    Task.Cancel;
  end;
end;

destructor TCalculationProject.Destroy;
begin
   List.Free;
  inherited;
end;

function TCalculationProject.CalculateListItem(const AIndex: Integer): Real;
begin
  //a function which takes a lot of calculation time
  //however in this example we simulate the calculation time and
  //use a simple alogorithm to verify the list afterwards
  Sleep(30);
  Result:=10*AIndex;
end;

procedure TCalculationProject.CalculateList(NotifyCompleted: TNotifyEvent);
begin
  if not Assigned(List) then
    List := TList<Real>.Create;

  List.Clear;

  Task:= TTask.Run(
    procedure
    var
      LoopResult : TParallel.TLoopResult;
      Lock : TCriticalSection;
    begin
      Lock:= TCriticalSection.Create;
      try
        LoopResult:= TParallel.&For(0, 1000-1,
          procedure(AIndex: Integer; LoopState: TParallel.TLoopState)
          var
            Res: Real;
          begin
            if (Task.Status=TTaskStatus.Canceled) and not(LoopState.Stopped) then
            begin
              LoopState.Stop;
            end;
            if LoopState.Stopped then
            begin
              Exit;
            end;

            Res:= CalculateListItem(AIndex);
            Lock.Enter;
            try
              List.Add(Res);
            finally
              Lock.Leave;
            end;
          end);
      finally
        Lock.Free;
      end;

      if (Task.Status = TTaskStatus.Canceled) then
        TThread.Synchronize(TThread.Current,
          procedure
          begin
            List.Clear;
          end)
      else
      if LoopResult.Completed then
        TThread.Synchronize(TThread.Current,
         procedure
         begin
           SortList;
           ShowList;
         end);
      // Notify the main thread that the task is ended
      TThread.Synchronize(nil,  // Or TThread.Queue
        procedure
        begin
          NotifyCompleted(Self);
        end);
    end
  );
end;
Run Code Online (Sandbox Code Playgroud)

用户界面调用:

procedure TMyForm.StartCalcClick(Sender: TObject);
begin
  StartCalc.Enabled := false;
  CalcObj.CalculateList(TaskCompleted);
end;

procedure TMyForm.TaskCompleted(Sender: TObject);
begin
  StartCalc.Enabled := true;
end;
Run Code Online (Sandbox Code Playgroud)

在评论中,似乎用户希望在一个操作中触发取消和新任务而不被阻止。

要解决这个问题,请将标志设置为 true,然后对任务调用取消。当TaskCompleted事件被调用时,检查标志,如果设置,则启动一个新任务。使用TThread.Queue()任务来触发TaskCompleted事件。