How do I stop a running TTask thread-safe?
In Delphi 10.1 Berlin, I would like to add the ability to stop the sensitive TParallel. & for the loop from my question How to make TParallel. & values โโin TList <T>? ...
The loop calculates the values โโand stores those values โโin TList. It runs on a separate thread with TTask.Run to make it responsive:
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;
The current running design task should be stopped when
- the calculation is restarted
- user clicks cancel button
I added
if Assigned(Task) then
begin
Task.Cancel;
end;
at the beginning procedure TCalculationProject.CalculateList
and in procedure TCalculationProject.CancelButtonClicked
, which is called when the cancel button is pressed.
The cycle is stopped with
if (Task.Status=TTaskStatus.Canceled) and not(LoopState.Stopped) then
begin
LoopState.Stop;
end;
if LoopState.Stopped then
begin
Exit;
end;
and the list is cleared with
if (Task.Status=TTaskStatus.Canceled) then
begin
TThread.Synchronize(TThread.Current,
procedure
begin
List.Clear;
end
);
end
It doesn't work when I restart the calculation. Then two calculation tasks are performed. I tried adding Task.Wait
after Task.Cancel
to wait for the task to complete before we start a new calculation, but without success.
What is the correct completely safe flow to implement such an undo / stop function?
source to share
The reason Wait
not working is deadlock. The call Synchronize
and Wait
effectively stops the execution of the task in progress.
If you change all calls Synchronize
to Queue
, you will avoid blocking. But the call Task.Cancel
in combination with Task.Wait
in the task being executed will throw an error EOperationCancelled
, so no luck there.
Update: This has been reported as a bug and is still not fixed in Delphi 10.2.3 Tokyo. https://quality.embarcadero.com/browse/RSP-11267
To fix this particular issue, you need to get notified when it Task
ended, either through completion, or canceled or stopped.
When the task starts, the user interface should block any attempt to start a new calculation until the first one is ready / canceled.
- First, when the calculation task is running, disable the button that will start a new calculation.
- Second, sync or queue the call to enable the button at the end of the task.
There is now a safe way to know when a task is completed / stopped or canceled. When doing so, remove the operator if Assigned(Task) then Task.Cancel
in the method CalculateList
.
If the method CalculateListItem
is taking a long time, consider checking the cancellation status flag regularly.
Example:
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;
And the user interface:
procedure TMyForm.StartCalcClick(Sender: TObject);
begin
StartCalc.Enabled := false;
CalcObj.CalculateList(TaskCompleted);
end;
procedure TMyForm.TaskCompleted(Sender: TObject);
begin
StartCalc.Enabled := true;
end;
In the comment, it looks like the user would like to trigger cancellation and new task in one operation without blocking.
To solve this problem, set the flag to true, cancel the task call. When the event is triggered TaskCompleted
, check the flag and, if set, start a new task. Use TThread.Queue()
from task to trigger an event TaskCompleted
.
source to share
Cancellation is broken in System.Threading. See https://quality.embarcadero.com/browse/RSP-11267
The following works by using a different mechanism to signal the termination of threads (StopRunning). Note the use of LoopState.Break and LoopState.ShouldExit. Note also the use of a queue instead of Synchronize. This allows us to wait for the task on the main thread without blocking.
To use the code, you need a form with a ListBox1 and two buttons btnStart and btnCancel.
type
TForm1 = class(TForm)
btnStart: TButton;
btnCancel: TButton;
ListBox1: TListBox;
procedure btnStartClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure btnCancelClick(Sender: TObject);
private
{ Private declarations }
private
Task: ITask;
public
{ Public declarations }
List: TList<Double>;
StopRunning : Boolean;
function CalculateListItem(const AIndex: Integer): Real;
procedure CalculateList;
procedure ShowList;
end;
var
Form1: TForm1;
implementation
uses
System.SyncObjs;
{$R *.dfm}
function TForm1.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 TForm1.FormCreate(Sender: TObject);
begin
List := TList<Double>.Create;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
List.Free;
end;
procedure TForm1.ShowList;
Var
R : Double;
begin
for R in List do
ListBox1.Items.Add(R.ToString);
end;
procedure TForm1.CalculateList;
Var
R : Real;
begin
List.Clear;
if Assigned(Task) then
begin
Task.Cancel;
end;
StopRunning := False;
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: Double;
begin
if StopRunning then begin
LoopState.Break;
Exit;
end;
if LoopState.ShouldExit then
Exit;
Res:=CalculateListItem(AIndex);
Lock.Enter;
try
List.Add(Res);
finally
Lock.Leave;
end;
end
);
finally
Lock.Free;
end;
if LoopResult.Completed then
TThread.Queue(TThread.Current,
procedure
begin
List.Sort;
ShowList;
end
)
else
TThread.Queue(TThread.Current,
procedure
begin
List.Clear;
ListBox1.Items.Add('Cancelled')
end
);
end
);
end;
procedure TForm1.btnCancelClick(Sender: TObject);
begin
StopRunning := True;
Task.Wait;
end;
procedure TForm1.btnStartClick(Sender: TObject);
begin
ListBox1.Clear;
CalculateList;
end;
source to share
Based on @ pyscripters answer, I tried to encapsulate the functionality in a class and call the functions of that class from the UI.
- Task launch works
- Stop + start a task while it is running
- Closing a form while a task is running
The final hint was to add CheckSynchronize to the Shutdown method.
unit uCalculation2;
interface
uses
System.Classes,
System.Generics.Collections,
System.Threading;
type
TNotifyTaskEvent = procedure(Sender: TObject; AMessage: string) of object;
TCalc2 = class
private
FTask : ITask;
FOnNotifyTaskEvent: TNotifyTaskEvent;
FCancelTask : Boolean;
function CalculateListItem(const AIndex: Integer): Real;
procedure CalculateList;
procedure DoNotify(AMessage: string);
public
List: TList<Double>;
constructor Create;
destructor Destroy; override;
procedure Start;
procedure Stop;
property OnNotifyTaskEvent: TNotifyTaskEvent read FOnNotifyTaskEvent write FOnNotifyTaskEvent;
end;
implementation
uses
System.SysUtils,
System.SyncObjs;
constructor TCalc2.Create;
begin
List := TList<Double>.Create;
end;
destructor TCalc2.Destroy;
begin
FOnNotifyTaskEvent := Nil;
Stop;
CheckSynchronize;
FTask := Nil;
List.Free;
inherited;
end;
procedure TCalc2.DoNotify(AMessage: string);
begin
if Assigned(FOnNotifyTaskEvent) then
begin
if Assigned(FTask) then
AMessage := Format('%4d: %-40s Entries=%3d', [FTask.Id, AMessage, List.Count])
else
AMessage := Format('%4d: %-40s Entries=%3d', [0, AMessage, List.Count]);
FOnNotifyTaskEvent(Self, AMessage);
end;
end;
function TCalc2.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 TCalc2.CalculateList;
begin
List.Clear;
if Assigned(FTask) then
begin
FTask.Cancel;
end;
FCancelTask := False;
FTask := TTask.Run(
procedure
var
LoopResult: TParallel.TLoopResult;
Lock: TCriticalSection;
begin
// TThread.Queue(TThread.Current,
// procedure
// begin
// DoNotify('Started');
// end
// );
Lock := TCriticalSection.Create;
try
LoopResult := TParallel.For(0, 500 - 1,
procedure(AIndex: Integer; LoopState: TParallel.TLoopState)
var
Res: Double;
begin
if FCancelTask then
begin
LoopState.Break;
Exit;
end;
if LoopState.ShouldExit then
Exit;
Res := CalculateListItem(AIndex);
Lock.Enter;
try
List.Add(Res);
finally
Lock.Leave;
end;
end
);
finally
Lock.Free;
end;
if LoopResult.Completed then
TThread.Queue(TThread.Current,
procedure
begin
DoNotify('Completed');
end
)
else
TThread.Queue(TThread.Current,
procedure
begin
DoNotify('Canceled');
end
);
end
);
end;
procedure TCalc2.Start;
begin
CalculateList;
end;
procedure TCalc2.Stop;
begin
FCancelTask := True;
if Assigned(FTask) then
FTask.Wait;
end;
end.
The calls from the UI look like this:
procedure TForm5.FormCreate(Sender: TObject);
begin
FCalc2 := TCalc2.Create;
FCalc2.OnNotifyTaskEvent := CalcCompleted;
end;
procedure TForm5.FormDestroy(Sender: TObject);
begin
FCalc2.Free;
end;
procedure TForm5.btnCalcCancelClick(Sender: TObject);
begin
FCalc2.Stop;
end;
procedure TForm5.btnCalcRunClick(Sender: TObject);
begin
CalcRun;
end;
procedure TForm5.btnRunAnotherClick(Sender: TObject);
begin
CalcRun;
end;
procedure TForm5.CalcCompleted(Sender: TObject; Status: string);
begin
memStatus.Lines.Add(Status);
btnCalcRun.Enabled := true;
end;
procedure TForm5.CalcRun;
begin
btnCalcRun.Enabled := false;
memStatus.Lines.Add('Started');
FCalc2.Stop;
FCalc2.Start;
end;
source to share