Ansatz für Task-Queue Sequenz
Hallo zusammen,
für die Separierung von Aufgaben in Taksk möchte ich anonyme Methoden benutzen, und diese dann mit kleinen Delays hintereinander ausführen. Ich möchte einfach eine leicht zu handhabende Funktion um innerhalb von Actions und Events eine Ausführung "Ohne externen Timer" zu starten, wenn man noch innerhalb der eigentlichen EventFunktion ist. Ein Beispiel wäre FireMonkey TabControl -> OnTabChange. Wenn in der OnTabChange ein weiterer TabChange ausgelöst werden soll wird das ignoriert, weil das nach Verlassen des EventFunktion wieder rückgesetzt wird. Also müsste ich EventFunktion erst beenden lassen, und dann von ausserhalb den TabChange machen den ich möchte. Ich habe einen ersten Versuch mit TTask und TTread.Synchronize gemacht, siehe hier:
Code:
// Procedure sequence of syncronized Tasks
// iDelay determines a delay before or after the Task, to separate external Events // -iDelay means Delay before each Task, already before first // +iDelay means Delay after each Task, the first is w/o delay procedure Task_Sequence_SyncAndDelayed(iDelay : Integer; const aProcs : TS4Proc_Delegate_Array); begin TTask.Create(procedure () var I : Integer; tsk : ITask; begin for I := 0 to Length(aProcs)-1 do begin tsk := TTask.Create(procedure () begin if iDelay < 0 then Sleep(Abs(iDelay)); // xx milliseconds TThread.Synchronize(nil, TThreadProcedure(aProcs[I]) ); //Interact with UI if iDelay > 0 then Sleep(Abs(iDelay)); // xx milliseconds end).Start; tsk.Wait(); end; // End loop end).Start; end; Aufgerufen werden soll sas dann wie folgt: Es können mehrere Aufgaben hintereinandergeschaltet werden, falls nötig.
Code:
Task_Sequence_SyncAndDelayed(-500,
[ // 1st Task procedure begin //Interact with UI FFrmNotebook.ClearHeader( True ); ChangeTabActionViewNotebook.ExecuteTarget(ChangeTabActionViewNotebook); end, // 2nd Task, separated by Delay procedure begin //Interact with UI ChangeTabActionViewProject.ExecuteTarget(ChangeTabActionViewProject); end ]); Das scheint so zu funktionieren, aber ich bin nicht sicher ob es auch grundsätzlich in Ordnung ist das so zu machen. Womöglich gibt es Probleme mit Delphi, Firemonkey, iOS, MAXOS, Android, etc. Vielleicht gibt es ja auch schon eine Task-Queue die durch Delays getrennt wird, welche ich nicht gefunden habe. Wäre schön eure Meinung dazu zu bekommen. Rollo |
AW: Ansatz für Task-Queue Sequenz
Falscher Ansatz - würde ich sagen -
Das ist ein klassischer Ansatz für Sir Rufo's Idleworker... Hole dir
Delphi-Quellcode:
Dann nimm eine TaskListe wie
TMessageManager.DefaultManager.SubscribeToMessage( TIdleMessage, HandleIdleMessage );
Delphi-Quellcode:
In die Queue geht es mit
FTasks: TQueue<TProc>;
Delphi-Quellcode:
und die Verarbeitung geht so:
procedure TIdleWorker.Execute( Action: TProc );
begin FTasks.Enqueue( Action ); end;
Delphi-Quellcode:
Ich habe "ALLE" Application.Processmessages aus meiner App entfernt...
procedure TIdleWorker.HandleIdleMessage( const Sender: TObject; const m: TMessage );
var LTask: TProc; begin if FTasks.Count > 0 then begin LTask := FTasks.Dequeue( ); LTask( ); end; end; Warum waren die da drin? Beispiel:
Delphi-Quellcode:
Weil Firemonkey sonst NIE die Click oder Select Animation abspielt.. (Farbe setzen usw.)
Procedure TForm1.Button1Click(Sender : TObject);
begin Application.Processmessages; MachewasLanges; end; Daher sieht der User keine Reaktion.. Daher mache ich meine Verarbeitung nur noch im Application.OnIdle -> der ruft die obere Message auf... Dann sieht es so aus:
Delphi-Quellcode:
Bedeutet die App ist sofort wieder im MainThread und kann die ganze UI aufbauen... Und wenn alles fertig ist meine Routine aufrufen...
Procedure TForm1.Button1Click(Sender : TObject);
begin TIdleWorker.default.Execute(Procedure begin MachewasLanges; end; end; So habe ich alles umgestellt... Mavarik |
AW: Ansatz für Task-Queue Sequenz
Hallo Mavarik,
dankesehr für die Vorschläge. Habe mir schon gedacht das es was in Richtung Enqueue geben müsste. Das mit Application.ProcessMessages sehe ich auch so, deshalb hoffe ich das Delay() entsprechend sauber in allen OS umgesetzt wird. Ich möchte aber eben noch ein Delay mit einbauen, um die Ausführungen zu Verzögern, wenn ich statt Delay einen Performancetimer oder ähnliches nehme mache ich eine Loop die auch viel Rechenzeit für nichts verwendet. Ist da ProcessMessages nicht das kleinere übel ? Oder gibt es eine DelayFunktion die ohne ProcessMessages auskommt ? EDIT: Übrigens Zitat:
Rollo |
AW: Ansatz für Task-Queue Sequenz
Man kann dem IdleWorker noch zwei Arrays verpassen:
Delphi-Quellcode:
FTasks : T??? // 0..n
EarliestStartArr : Array of Integer // 0..n (GetTickCount+gewünschtes Delay) WaitAfterExecArr : Array of Integer // 0..n
Delphi-Quellcode:
procedure TIdleWorker.Execute( Action: TProc ; EarliestStart : Integer; WaitAfterExec : Integer);
begin FTasks.Enqueue( Action ); Setlength(EarliestStartArr,Length(EarliestStartArr )+1); EarliestStartArr[High(EarliestStartArr )] := EarliestStart; Setlength(WaitAfterExecArr ,Length(WaitAfterExecArr)+1); WaitAfterExecArr[High(WaitAfterExecArr)] := WaitAfterExec; end;
Delphi-Quellcode:
Nicht getestet, nur im Browser getippt.
procedure TIdleWorker.HandleIdleMessage( const Sender: TObject; const m: TMessage );
var LTask: TProc; begin if FTasks.Count > 0 then begin LTask := FTasks.Dequeue( ); while GetTickCount<EarliestStartArr[??] do sleep(1); LTask( ); sleep(WaitAfterExecArr[??]) // hier noch die Arrays anpassen: x[??] löschen und Längen dementsprechend anpassen end; end; |
AW: Ansatz für Task-Queue Sequenz
Du willst also etwas nach einer bestimmten Zeit im MainThread abarbeiten lassen.
Dann nimmt man sich eine Liste und fügt diese Methoden dort ein. Die Liste sortiert dann die Einträge nach den Delay-Werten (bzw. dem Zeitpunkt, ab wann diese Methode ausgeführt werden soll). Jedes Mal, wenn die Anwendung in den Idle-Zustand kommt, dann führt man die einfach die Methode aus, die jetzt an der Reihe ist. Der Vorteil dabei ist, dass du damit garantieren kannst, dass diese Methoden auch ausgeführt werden. Bei den Tasks geht das eben nicht (wenn man eine bestimmte Grenze überschreitet). Denn je nach CPU-Anzahl sind immer nur eine begrenzte Anzahl an Worker-Threads aktiv. Und wenn du nun dort einen Haufen Tasks einstellst, die mal eben 5 Minuten warten sollen und dann einen Task, der 10 Sekunden warten soll, dann kann es dir u.U. sogar passieren, dass dein 10 Sekunden Task erst nach ein paar Stunden aufgerufen wird! Die TPL funktioniert dort gut, wo ich Tasks habe, die auch ihre echte Arbeit aufnehmen, wenn diese gestartet wurden und diese Arbeit auch ein Anfang und Ende hat. Langläufer oder Langschläfer haben in der TPL nichts zu suchen, denn dafür ist diese nicht gebaut. |
AW: Ansatz für Task-Queue Sequenz
@Photoner
So definitiv nicht, sondern eine Liste aus so einem kleinen Record:
Delphi-Quellcode:
Eine Queue ist hier nicht mehr möglich, da man die Verarbeitung anhand der Startzeiten steuern möchte.
TWaitMethod = record
Proc : TProc; ExecuteAfter : TDateTime; end; |
AW: Ansatz für Task-Queue Sequenz
@Sir Rufo
Stimmt. Das würde blockieren. Wie wäre es damit: In HandleIdleMessage müsste man erst die EarliestStart Liste durchgehen und die erste Methothe die die Bedingung erfüllt wird ausgeführt. Erfüllt das keine, dann ists kein Problem mehr |
AW: Ansatz für Task-Queue Sequenz
Hallo Photoner,
danke für den Vorschlag. Aber da ist auch ein Sleep() drin. Wenn ich Mavarik richtig verstehe vermutet er darin ein Application.ProcessMessages, und das möchte er mit Recht verbannen. Da bleibt wohl nur mit GetTickCount zu selber zu zählen. Aber ist GetTickCount Platform-Spezifisch ? @Sir Rufo Ja, so ungefähr. Es sollte aber auch ein möglichst einfache Syntax sein. Also am Beispiel ChangeTabAction.ExecuteTarget(), da diese Zeile im Aufrufe Probleme macht möchte ich die etwas verzögern damit es nach dem Aufrufer ausgeführt wird. - ist nicht unbedingt etwas Langes - soll aber kein Syntax-Monster werden - Wenn möglich auch gleicht mehrere solcher Prags nacheinander schalten können, um z.B. Tabs mehrecht gleiten zu lassen Rollo |
AW: Ansatz für Task-Queue Sequenz
Du willst doch Dein Delay nur deswegen machen, damit das Tabchange usw. noch ausgeführt wird.. oder habe ich das falsch verstanden?
Da bei sowas die UITask beschäftigt ist, wird der onIdle sowieso nicht aufgerufen und warten ganz automatisch... Der Delay müsste ja processorabhängig sein. Mavarik |
AW: Ansatz für Task-Queue Sequenz
Sleep function
https://msdn.microsoft.com/en-us/lib...=vs.85%29.aspx kernel32.dll "Suspends the execution of the current thread until the time-out interval elapses." System.Sysutils gibt noch jede Menge Fktn. her. Stichworte:
|
AW: Ansatz für Task-Queue Sequenz
Also hier mal so ein IdleWorker, der dann auch die Delays berücksichtigt (ACHTUNG: So nur für FMX, für die VCL müsste man die IdleMessage selber verschicken)
Delphi-Quellcode:
und ein kleiner Testaufruf:
unit IdleWorker;
interface uses System.Generics.Collections, System.Generics.Defaults, System.Messaging, System.SysUtils, System.TimeSpan, System.DateUtils; type TIdleWorker = class private const MinWorkingTime = 20; DefaultWorkingTime = 50; type TTask = record Action: TProc; ExecuteAfter: TDateTime; end; private FTasks: TList<TTask>; FWorkingTime: Cardinal; procedure SetWorkingTime( const Value: Cardinal ); protected procedure HandleIdleMessage( const Sender: TObject; const m: TMessage ); public constructor Create( ); destructor Destroy; override; procedure Execute( Action: TProc ); overload; procedure Execute( Action: TProc; ADelay: TTimeSpan ); overload; procedure Execute( Action: TProc; ADelay: Cardinal ); overload; procedure Execute( Action: TProc; AExecuteAfter: TDateTime ); overload; property WorkingTime: Cardinal read FWorkingTime write SetWorkingTime default DefaultWorkingTime; private class var _Default: TIdleWorker; protected class destructor Destroy; public class function Default: TIdleWorker; end; implementation uses System.Diagnostics, FMX.Types; { TIdleWorker } constructor TIdleWorker.Create; begin inherited; FWorkingTime := DefaultWorkingTime; FTasks := TList<TTask>.Create( TComparer<TTask>.Construct( function( const L, R: TTask ): integer begin Result := CompareDateTime( R.ExecuteAfter, L.ExecuteAfter ); end ) ); TMessageManager.DefaultManager.SubscribeToMessage( TIdleMessage, HandleIdleMessage ); end; class function TIdleWorker.Default: TIdleWorker; begin if not Assigned( _Default ) then _Default := TIdleWorker.Create( ); Result := _Default; end; class destructor TIdleWorker.Destroy; begin FreeAndNil( _Default ); end; destructor TIdleWorker.Destroy; begin TMessageManager.DefaultManager.Unsubscribe( TIdleMessage, HandleIdleMessage ); FTasks.Free; inherited; end; procedure TIdleWorker.Execute( Action: TProc; ADelay: TTimeSpan ); begin Execute( Action, Now + ADelay ); end; procedure TIdleWorker.Execute( Action: TProc ); begin Execute( Action, Now ); end; procedure TIdleWorker.Execute( Action: TProc; AExecuteAfter: TDateTime ); var LTask: TTask; begin LTask.Action := Action; LTask.ExecuteAfter := AExecuteAfter; FTasks.Add( LTask ); FTasks.Sort( ); end; procedure TIdleWorker.HandleIdleMessage( const Sender: TObject; const m: TMessage ); var LTask: TTask; LSW: TStopwatch; begin LSW := TStopwatch.StartNew( ); while ( LSW.ElapsedMilliseconds < FWorkingTime ) do begin if ( FTasks.Count > 0 ) and ( FTasks.Last.ExecuteAfter <= Now ) then begin LTask := FTasks.Extract( FTasks.Last ); LTask.Action( ); end else Break; end; end; procedure TIdleWorker.SetWorkingTime( const Value: Cardinal ); begin if Value >= MinWorkingTime then FWorkingTime := Value; end; procedure TIdleWorker.Execute( Action: TProc; ADelay: Cardinal ); begin Execute( Action, IncMilliSecond( Now, ADelay ) ); end; end.
Delphi-Quellcode:
Nachtrag
unit Form.Main;
interface uses System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants, FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Controls.Presentation, FMX.StdCtrls, FMX.Layouts, FMX.ListBox; type TForm1 = class( TForm ) ListBox1: TListBox; Button1: TButton; procedure Button1Click( Sender: TObject ); private { Private-Deklarationen } public { Public-Deklarationen } end; var Form1: TForm1; implementation {$R *.fmx} uses System.TimeSpan, IdleWorker; procedure TForm1.Button1Click( Sender: TObject ); begin TIdleWorker.Default.Execute( procedure begin ListBox1.Items.Add( 'Sofort' ); end ); TIdleWorker.Default.Execute( procedure begin ListBox1.Items.Add( 'nach 750 Millisekunden' ); end, 750 ); TIdleWorker.Default.Execute( procedure begin ListBox1.Items.Add( 'nach 3 Sekunden' ); end, TTimeSpan.FromSeconds( 3 ) ); end; end. Das ist die Unit, damit die IdleWorker auch unter VCL läuft
Delphi-Quellcode:
Mit der VCL sieht das Beispiel dann so aus:
unit IdleWorker.VclBroker;
interface implementation uses FMX.Types, System.Classes, System.Messaging, Vcl.Forms, Vcl.AppEvnts; type TVclIdleMessageBroker = class( TComponent ) private FAppEvents: TApplicationEvents; procedure AppEventsOnIdle( Sender: TObject; var Done: Boolean ); public procedure AfterConstruction; override; end; { TVclIdleMessageBroker } procedure TVclIdleMessageBroker.AfterConstruction; begin inherited; FAppEvents := TApplicationEvents.Create( Self ); FAppEvents.OnIdle := AppEventsOnIdle; end; procedure TVclIdleMessageBroker.AppEventsOnIdle( Sender: TObject; var Done: Boolean ); begin TMessageManager.DefaultManager.SendMessage( Self, TIdleMessage.Create( ) ); end; initialization TVclIdleMessageBroker.Create( Application ); end.
Delphi-Quellcode:
unit Form.Main;
interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls; type TMainForm = class(TForm) Button1: TButton; ListBox1: TListBox; procedure Button1Click(Sender: TObject); private { Private-Deklarationen } public { Public-Deklarationen } end; var MainForm: TMainForm; implementation {$R *.dfm} uses System.TimeSpan, IdleWorker, IdleWorker.VclBroker {<- den hier nicht vergessen}; procedure TMainForm.Button1Click(Sender: TObject); begin TIdleWorker.Default.Execute( procedure begin ListBox1.Items.Add( 'Sofort' ); end ); TIdleWorker.Default.Execute( procedure begin ListBox1.Items.Add( 'nach 750 Millisekunden' ); end, 750 ); TIdleWorker.Default.Execute( procedure begin ListBox1.Items.Add( 'nach 3 Sekunden' ); end, TTimeSpan.FromSeconds( 3 ) ); end; end. |
AW: Ansatz für Task-Queue Sequenz
@Sir Rufo:
Kleinigkeit: -TMessageManager ist in XE5 in der FMX.Messages. XE6 u. XE7 System.Messaging Ansonsten Kudos! TIdleWorker löst Probleme die einen schier zur Verzweiflung bringen. Bsp.:
Delphi-Quellcode:
procedure TForm1.Edit1Enter(Sender: TObject);
begin IdleWorker.TIdleWorker.Default.Execute( procedure begin TEdit(Sender).SelectAll; end); end; |
AW: Ansatz für Task-Queue Sequenz
@Sir Rufo
Dankesehr für die Mühe, das sieht perfekt aus :-) Und das sollte genau für mein Problem passen. Rollo |
AW: Ansatz für Task-Queue Sequenz
Zitat:
Zitat:
|
AW: Ansatz für Task-Queue Sequenz
Zitat:
Dem Lob möchte ich mich anschließen, saubere Lösung. Funktioniert auf allen FMX-Plattformen (also incl. den mobilen) und eine Variante für reines VCL ist sogar auch dabei. :thumb::thumb::thumb: |
Alle Zeitangaben in WEZ +1. Es ist jetzt 07:51 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz