14.06.2023, Vladimír Klaus, navštíveno 575x

Delphi

V tomto článku se podíváme na výhody paralelního programování pomocí TTask, ale také na problémy, které jsou s tím spojeny. Vše si budeme ukazovat na fiktivní náročné/dlouhotrvající proceduře "NarocnaUloha", kterou v běžné aplikaci může uživatel spustit například tlačítkem a pak čeká na její výsledek, který se vypíše třeba do komponenty TLabel.

procedure TForm1.NarocnaUloha;
begin
  Label1.Caption:='Spuštěno...';
  //...něco, co dlouho trvá...
  Label1.Caption:='Dokončeno';
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  NarocnaUloha;
end;

Přepsání do "paralelního programování", je velice jednoduché. Využijeme k tomu unitu System.Threading, která obsahuje třídu TTask. A všechno, co bude "uvnitř" objektu se provede paralelně - tedy v jiném vlákně - a hlavní aplikace bude moci dál fungovat.

uses
  System.Threading;
var
  myTask: ITask;

procedure TForm1.Button1Click(Sender: TObject);
begin
  //vytvoříme (připravíme) paralelní úlohu, do jejíhož "těla" vložíme tu naši náročnou úlohu
  myTask:=TTask.Create(procedure ()
    begin
      NarocnaUloha;
    end);
  //a spustíme (v jiném vlákně)
  myTask.Start;
end;

Ovlivnění uživatelského prostředí

Problematické chování

Jak velice záhy zjistíte, dojde při spuštění k chybě. Důvodem je to, že do uživatelského prostředí může zapisovat nebo něco zobrazovat pouze hlavní vlákno, ve kterém běží vlastní aplikace. Jakékoliv paralelní úlohy nesmí s uživatelem jakkoli interagovat.

Poznámka k Delphi 11 (možná i pro starší): Může se vám stát, že k žádné chybě nedojde a vše bude správně fungovat, i když ovlivňujete uživatelské prostředí z paralelního vlákna. Je to zřejmě způsobeno vylepšením implementace System.Threading, ale i tak to nelze v žádném případě doporučit. Ve chvíli, kdy takto spustíte více vláken (ovlivňujících UI) nebo obecně dojde k dalším změnám, může to vést k nekonzistentnímu chování, nežádoucím efektům nebo opravdovým pádům/zamrznutí aplikace!

Bude tedy třeba odstranit vypisování do Labelu. Přesněji řečeno, chtělo by to dát před začátek paralelní úlohy a za konec. Tím se chyba odstraní, ale problém to nevyřeší. Na začátku se sice vypíše "Spuštěno...", ale ihned se přepíše nápisem "Dokončeno". Důvod je jasný - pomocí "myTask.Start;" spustíme úlohu v jiném vlákně a aplikace okamžitě pokračuje dál.

procedure TForm1.Button1Click(Sender: TObject);
begin
  Label1.Caption:='Spuštěno...';
  myTask:=TTask.Create(procedure ()
    begin
      NarocnaUloha;
    end);
  myTask.Start; // <<< spustí se úloha v paralelním vlákně a ihned se pokračuje dál
  Label1.Caption:='Dokončeno';
end;

Jak tedy čekat na dokončení úlohy a zároveň nebránit hlavní aplikaci v činnosti? Můžeme k tomu použít zaslání zprávy (PostMessage), na kterou bude reagovat hlavní aplikace a až v obslužné proceduře vypíše "Dokončeno". Ale to je dost práce, navíc zbytečné, protože naštěstí existuje jednodušší řešení.

procedure TForm1.Button1Click(Sender: TObject);
begin
  Label1.Caption:='Spuštěno...';
  myTask:=TTask.Create(procedure ()
    begin
      NarocnaUloha;
      //dále zařadíme do speciální fronty anonymní proceduru,
      //která výpis provede za nás, ale až po ukončení paralelního tasku
      TThread.Queue (nil, procedure
                          begin
                            Label1.Caption:='Dokončeno';
                          end);
    end);
  myTask.Start;
end;

Opakované spuštění

Dalším problémem je, že pokud je aplikace "živá", umožňuje na uvedené tlačítko znovu kliknout. Což samozřejmě někdy můžeme chtít, ale spíše ne. Řešením je testování, zda už není task přiřazen.

procedure TForm1.Button1Click(Sender: TObject);
begin
  if Assigned(myTask) then exit;

  Label1.Caption:='Spuštěno...';
  //... atd.
end;

Další variantou je tlačítko ihned znepřístupnit a teprve po dokončení ho zase uvolnit.

Co je třeba také řešit

  • Ukončení aplikace - Pokud implementujete výše uvedené řešení, hlavní aplikace není blokovaná a to znamená bohužel i to, že se ji můžete pokusit zavřít.
  • Výjimky - máte je nějak ošetřené zápisem do souboru/databáze nebo se zobrazí chybová hláška, tedy opět dojde k nepřípustnému ovlivnění uživatelského prostředí?
  • Více vláken, které se mohou ovlivňovat - Umožňujete spustit více úloh/vláken, které se ovlivňují, čekají vzájemně na výsledek nebo něco takového?

Závěr

S paralelním zpracování je mnohem více legrace, protože metod, jak k tomu přistupovat, je více, např. Future, Parallel.For apod. Některé z dalších záležitostí se pokusím rozebrat v pokračování tohoto zajímavého tématu.

Zdroje: