2016-09-19 4 views
4

SITUATIONDelphi korrekte Verwendung von Aufgaben

Zum besseren Verständnis der PPL und wie ein Task Arbeiten habe ich versucht, ein sehr einfaches Programm, in dem machen, wenn Sie auf eine Schaltfläche geklickt haben, ein ListBox mit einer Liste gefüllt ist von Verzeichnissen auf der Festplatte.

procedure TForm3.Button1Click(Sender: TObject); 
var proc: ITask; 
begin 

//Show that something is going to happen 
Button1.Caption := 'Process...'; 

proc := TTask.Create(

    procedure 
    var strPath: string; 
     sl: TStringDynArray; 
    begin 

    if (DirectoryExists('C:\Users\albertoWinVM\Documents\uni\maths')) then 
    begin 
    ListBox1.Items.Clear; 
    sl := TDirectory.GetDirectories('C:\Users\albertoWinVM\Documents\uni\maths', 
    TSearchOption.soAllDirectories, nil); 

    for strPath in sl do 
     begin 
     ListBox1.Items.Add(strPath); 
     end; 

    //At the end of the task, I restore the original caption of the button 
    Button1.Caption := 'Go'; 
    Label1.Caption := 'Finished'; 

    end; 
    end 

); 

proc.Start; 

end; 

Der Ordner maths Sie oben ist nicht sehr groß sehen und die Aufgabe dauert ca. 3 Sekunden auszuführen. Die Aufgabe wird wie folgt deklariert:

type 
    TForm3 = class(TForm) 
    ListBox1: TListBox; 
    //... other published things var ... 
    private 
    proc: ITask; 
    public 
    //... public var ... 
    end; 

PROBLEM

Wenn ich (zum Beispiel) arbeiten mit C:\Users\albertoWinVM\Documents Ich habe eine sehr große Anzahl von Ordnern und das Programm dauert bis zu 3 Minuten vor Füllen der ListBox.

Wenn ich das Programm geschlossen (während der Aufgabe noch ausgeführt wird) über den Code nur mit dem, was ich online verstanden habe gelesen hat, wird nach wie vor die Aufgabe ausgeführt, bis er nicht beendet ist. Hab ich recht?

Ich dachte, dass ich diesen Code hinzufügen könnte, um die Sicherheit des Programms zu verbessern. Ist das genug?

Antwort

6

TTask läuft in einem Arbeiter-Thread. Wie gezeigt, ist Ihr Task-Code nicht Thread-sicher. Sie müssen mit dem Haupt-UI-Thread synchronisieren, wenn Sie auf UI-Steuerelemente zugreifen.

Sie verwalten nicht Ihre proc Variable korrekt.Sie haben eine proc Variable als Mitglied Ihrer TForm3 Klasse deklariert, aber Sie auch haben eine lokale proc Variable in Ihrer Button1Click() Methode deklariert. Die Methode weist der lokalen Variablen die neue Aufgabe zu, das Klassenmitglied wird niemals zugewiesen.

Und nein, es ist nicht genug, nur Cancel() auf dem TTask zu rufen. Ihre Task-Prozedur muss regelmäßig überprüfen, ob die Task abgebrochen wurde, so dass sie ihre Arbeit stoppen kann (die einzige Möglichkeit zum Abbrechen von TDirectory.GetDirectories() ist, dass der Prädikatfilter eine Ausnahme auslöst).

Da TDirectory.GetDirectories() nicht beendet, bis alle Verzeichnisse wurden in der zurückgegebenen Liste befindet und gespeichert, wenn Sie eine verantwortungsvolle Aufgabe und ein schnelleres UI Ergebnis benötigen, oder wenn Sie nur die Speichernutzung reduzieren mögen, sollten Sie verwenden FindFirst()/FindNext() in einer manuellen Schleife statt, dann können Sie die Benutzeroberfläche aktualisieren und nach Bedarf zwischen den Schleifeniterationen abbrechen.

das gesagt ist, versuchen, etwas mehr wie folgt aus:

type 
    TForm3 = class(TForm) 
    ListBox1: TListBox; 
    //... 
    private 
    proc: ITask; 
    procedure AddToListBox(batch: TStringDynArray); 
    procedure TaskFinished; 
    public 
    //... 
    end; 

procedure TForm3.Button1Click(Sender: TObject); 
begin 
    if Assigned(proc) then 
    begin 
    ShowMessage('Task is already running'); 
    Exit; 
    end; 

    //Show that something is going to happen 
    Button1.Caption := 'Process...'; 

    proc := TTask.Create(
    procedure 
    var 
     strFolder: string; 
     sr: TSearchRec; 
     batch: TStringDynArray; 
     numInBatch: Integer; 
    begin 
     try 
     strFolder := 'C:\Users\albertoWinVM\Documents\uni\maths\'; 
     if FindFirst(strFolder + '*.*', faAnyFile, sr) = 0 then 
     try 
      TThread.Queue(nil, ListBox1.Items.Clear); 
      batch := nil; 

      repeat 
      Form3.proc.CheckCanceled; 

      if (sr.Attr and faDirectory) <> 0 then 
      begin 
       if (sr.Name <> '.') and (sr.Name <> '..') then 
       begin 
       if not Assigned(batch) then 
       begin 
        SetLength(batch, 25); 
        numInBatch := 0; 
       end; 

       batch[numInBatch] := strFolder + sr.Name; 
       Inc(numInBatch); 

       if numInBatch = Length(batch) then 
       begin 
        TThread.Queue(nil, 
        procedure 
        begin 
         AddToListBox(batch); 
        end 
        end); 

        batch := nil; 
        numInBatch := 0; 
       end; 
       end; 
      end; 
      until FindNext(sr) <> 0; 
     finally 
      FindClose(sr); 
     end; 

     if numInBatch > 0 then 
     begin 
      SetLength(batch, numInBatch) 
      TThread.Queue(nil, 
      procedure 
      begin 
       AddToListBox(batch); 
      end 
      end); 
     end; 
     finally 
     TThread.Queue(nil, TaskFinished); 
     end; 
    end 
); 
    proc.Start; 
end; 

procedure AddToListBox(batch: TStringDynArray); 
begin 
    ListBox1.Items.AddStrings(batch); 
end; 

procedure TForm3.TaskFinished; 
begin 
    proc := nil; 
    Button1.Caption := 'Go'; 
    Label1.Caption := 'Finished'; 
end; 

procedure TForm3.FormDestroy(Sender: TObject); 
begin 
    if Assigned(proc) then 
    begin 
    proc.Cancel; 
    repeat 
     if not proc.Wait(1000) then 
     CheckSynchronize; 
    until proc = nil; 
    end; 
end; 
+0

Für große Verzeichnislisten (oder jede große Liste) kann es auf der Benutzeroberfläche zu Batch-Updates nützlich sein. Die Aufgabe kann einige Dutzend Elemente sammeln und dann mit "BeginUpdate" und "EndUpdate" in der Listbox stapelweise aktualisieren. Das Aktualisieren jedes Elements kann ein Performance-Schwein sein. Dies erzeugt natürlich auch ein schönes Intervall zum Aufrufen von 'CheckCanceled', so dass '.Cancel' es' proc.Wait() 'erlaubt, rechtzeitig zurückzukehren. –

+0

Ich dachte, dass vor dem Posten, aber das wirft Probleme mit Speicherverwaltung und Objektbesitz. Aber ich habe meine Antwort aktualisiert. –

+0

Ich habe nicht erwartet, dass Sie Ihre Antwort aktualisieren - wurde nur für OP als eine Idee verlassen. Ich habe es aus demselben Grund nicht in meine Antwort aufgenommen; alles wird ziemlich schnell kompliziert. +1, auf jeden Fall! –

1

Sie können nicht UI-Objekte in Threads anders als der Haupt-Thread manipulieren. Sie müssen den Zugriff auf diese Objekte synchronisieren. Alle Arten von unerwarteten (dh schlechten) Dingen beginnen zu passieren, wenn mehrere Threads gleichzeitig auf UI-Objekten arbeiten.

Zum Beispiel - extrahieren Sie die Arbeit, die Sie mit der daraus resultierenden Verzeichnisliste zu tun beabsichtigen, wenn Sie es haben und es in eine separate Methode setzen:

procedure TForm1.UpdateDirectoryList(AList : TStringDynArray); 
var 
    strPath : string; 
begin 
    ListBox1.Items.BeginUpdate; 
    ListBox1.Items.Clear; 
    for strPath in AList do ListBox1.Items.Add(strPath); 
    ListBox1.Items.EndUpdate;  
    Button1.Caption := 'Go'; 
    Label1.Caption := 'Finished'; 
end; 

Dann haben Ihre Aufgabe Warteschlange diese Methode für den UI-Thread auszuführen, wenn es Arbeit seine lang andauernde ergänzt:

procedure TForm1.Button1Click(Sender: TObject); 
var proc: ITask; 
begin 
    Button1.Caption := 'Process...'; 
    ListBox1.Items.Clear; 
    proc := TTask.Create(
    procedure 
    var 
     sl: TStringDynArray; 
    begin 
     if (DirectoryExists('C:\Users\albertoWinVM\Documents\uni\maths')) then 
     begin 
      sl := TDirectory.GetDirectories('C:\Users\albertoWinVM\Documents\uni\maths', 
             TSearchOption.soAllDirectories, nil); 
      TThread.Queue(nil, procedure 
          begin 
           UpdateDirectoryList(sl); 
          end); 
     end; 
    end); 
    proc.Start; 
end; 

diese Weise ist Ihre Aufgabe ausschließlich auf private Daten, die sie dann wieder in den Hauptthread, wenn es abgeschlossen ist Betrieb - niemand Schritte auf der jeweils anderen Zehen.

Wenn der Faden Cancelling es nicht ausreicht, einfach ITask.Cancel zu nennen - Sie müssen warten, bis es fertig zu stellen. Im Idealfall sollte Ihre Aufgabe in regelmäßigen Abständen .CheckCanceled anrufen, damit sie rechtzeitig beendet werden kann, wenn sie extern abgebrochen wird. CheckCanceled erhöhen wird EOperationCancelled, wenn die Aufgabe abgebrochen wurde, so dass Sie, dass und Ausgang so schnell wie möglich behandeln sollen. Wenn Sie so suchen, wie es @Remy vorgeschlagen hat, wird dies viel einfacher, da Sie bei jeder Schleifeniteration Gelegenheit haben, nach einer Stornierung zu suchen.