在Delphi 10.1柏林,我想补充一下停止响应TParallel的可能性。&amp; For循环来自我的问题How to make a TParallel.&For loop responsive and store values in a 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;
时应停止当前运行的计算任务
我添加了
if Assigned(Task) then
begin
Task.Cancel;
end;
在procedure TCalculationProject.CalculateList
的开头和procedure TCalculationProject.CancelButtonClicked
中,点击取消按钮时会调用。
使用
停止循环if (Task.Status=TTaskStatus.Canceled) and not(LoopState.Stopped) then
begin
LoopState.Stop;
end;
if LoopState.Stopped then
begin
Exit;
end;
并使用
清除列表if (Task.Status=TTaskStatus.Canceled) then
begin
TThread.Synchronize(TThread.Current,
procedure
begin
List.Clear;
end
);
end
重新开始计算时,这不起作用。然后运行两个计算任务。我尝试在Task.Wait
之后添加Task.Cancel
以等待任务完成,然后我们开始新计算但没有成功。
实现这种取消/停止功能的正确的完全线程安全方法是什么?
答案 0 :(得分:3)
Wait
不起作用的原因是死锁。 Synchronize
调用和Wait
有效地阻止正在运行的任务完成。
如果您将所有Synchronize
来电更改为Queue
,您将避免死锁。但是在正在运行的任务上与Task.Cancel
一起调用Task.Wait
会引发EOperationCancelled
错误,因此没有运气。
更新:这是一个错误,在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;
用户界面呼叫:
procedure TMyForm.StartCalcClick(Sender: TObject);
begin
StartCalc.Enabled := false;
CalcObj.CalculateList(TaskCompleted);
end;
procedure TMyForm.TaskCompleted(Sender: TObject);
begin
StartCalc.Enabled := true;
end;
在评论中,用户希望在一次操作中触发取消和新任务而不会被阻止。
要解决此问题,请将标志设置为true,对任务调用cancel。调用TaskCompleted
事件时,请检查标志,如果已设置,则启动新任务。使用任务中的TThread.Queue()
来触发TaskCompleted
事件。
答案 1 :(得分:1)
System.Threading中取消了。见https://quality.embarcadero.com/browse/RSP-11267
以下工作方法是使用另一种机制来发送信号以停止线程(StopRunning)。注意LoopState.Break和LoopState.ShouldExit的使用。另请注意使用Queue而不是Synchronize。这允许我们在不阻塞的情况下等待主线程上的任务。
要使用代码,您需要一个带有ListBox1的表单和两个按钮btnStart和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;
答案 2 :(得分:0)
在@pyscripters的基础上回答我尝试将功能封装在一个类中,并从UI调用该类的功能。
最后的提示是将CheckSynchronize添加到Shutdown方法。
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.
来自UI的调用如下所示:
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;