szálőrület bejegyzései

Multithreading, de minek? Példák a szálak teljesen felesleges használatára.

Csuka, róka

A TThread.Suspend eljárás igazából rosszul van elnevezve. A helyes név így hangzik: TThread.RandomlyHangTheProgram. Nem vicc, ezt a helyzet. A legjobb, amit tehetünk, ha egyáltalán nem használjuk, de talán még jobb, ha el is felejtjük.

A Suspend ugyanis megállítja a szálat, akármit is csinál az éppen. Fogva tart egy fájlt? Várólistára mindenkit. Belépett egy critical sectionbe? Nincs több belépés. Képzeljük csak el azt, ha a szál éppen memóriát foglalt. A memóriakezelő is osztott erőforrás, critical section védi a szálak versengése ellen. Ha a szál éppen fogva tartja a critical sectiont amikor a Suspend meghívódik, a memóriakezelő ettől kezdve zárva van minden arrajáró számára. Egy egyszerű szövegművelet is lehetlenné válik, ha ahhoz memóriafoglalás vagy felszabadítás kellene. Ha pedig éppen az a szál próbál memóriát foglalni, amelyik a Suspendet meghívta, és majd később a Resume-ot hívná, kialakul a csuka fogta róka esete, amit csak úgy hívunk: deadlock.

Az olvasóra bíznánk annak végiggondolását, hogy mi történik, ha meghívjuk a win32 API TerminateThread eljárását.


Szál és szál-osztály.

Szál vagy TThread objektum. Kezdő programozónak gyakran nem megy a fejébe elsőre, hogy hogy is vannak ezek a szálak és objektumok egymással. Hajlamosak vagyunk azt hinni, hogy ami a TThread része, az szálon fut, ami meg nem a TThread része, ez nem fut szálon (azaz a fő VCL szálon fut).

Nagyon fontos, és mindenki vésse az eszébe: a kódnak és a szálnak semmi köze egymáshoz! Bármely kódrészlet futhat szálról is és nem szálról is. Attól függ csupán, hogy hívjuk-e onnan. Akármilyen eljárást hívunk meg szálról, az a szálon fog lefutni. Akkor is, ha a fő ablak metódusa. És amit nem szálról hívunk, az pedig nem szálról hívódik, még ha a TThread metódusa is.

Nézzük példaként időrendben, hogy mi történik, ha létrehozunk egy TThread objektumot.

constructor TThread.Create(CreateSuspended: Boolean);
begin
  inherited Create;
  AddThread;
  FSuspended := CreateSuspended;
  FCreateSuspended := CreateSuspended;
  FHandle := BeginThread(nil, 0, @ThreadProc, Pointer(Self), 
    CREATE_SUSPENDED, FThreadID);
  if FHandle = 0 then
    raise EThread.CreateResFmt(@SThreadCreateError, 
      [SysErrorMessage(GetLastError)]);
end;

Emlékeztető: mivel ezt a fő VCL szál hívta meg, így ez is azon fut. A lényeg a BeginThread, amely a delphiben a szálak indítására szolgál. Paraméterben kapja meg az eljárást, amit egy másik szálon akarunk végrehajtani. Kap még egy paramétert, ami ahhoz kell, hogy ama bizonyos eljárásnak üzenni tudjunk valamit. Erre azért van szükség, mert amikor egy szálat létrehozunk, valahogy el kell neki juttatni az adatokat, amivel dolgozni fog. Ha nem is magukat az adatokat, de legalább azt, hogy hol találhatni. Ez akkor válik igazán érdekessé, ha több teljesen egyforma szálat akarunk beindítani, azaz ugyanaz kód fog futni több példányban. Mondjuk ez matematikai alkalmazás, amiben több párhuzamos munkaszál egy egy számot analizál. A szál paramétere például lehet a szám, amit vizsgálnia kell. Jelen esetben maga a TThread objektum lesz ez a paraméter (Self). Mivel a ThreadProc egy sima eljárás, máskülönben nem tudná, hogy melyik TThread objektum indította őt be. Nézzük csak a BeginThreadet.

function BeginThread(SecurityAttributes: Pointer; 
  StackSize: LongWord; ThreadFunc: TThreadFunc; Parameter: Pointer; 
  CreationFlags: LongWord; var ThreadId: LongWord): Integer;
var
  P: PThreadRec;
begin
  New(P);
  P.Func := ThreadFunc;
  P.Parameter := Parameter;
  IsMultiThread := TRUE;
  Result := CreateThread(SecurityAttributes, StackSize, 
    @ThreadWrapper, P, CreationFlags, ThreadID);
end;

A BeginThread tulajdonképpen csak keret a CreateThread körül. A CreateThread az a win32 API hívás, amely paraméterben kapja meg azt az eljárást, amit aztán már egy másik szálról fog futtatni. Tulajdonképpen az eljárás maga lesz a szál élete. Ahogy a megadott eljárás kilép, a szál tevékenysége végetér, és a szál megszűnik.

Észreveendő, hogy a Delphi nem a mi általunk adott eljárást adja tovább, hanem egy saját eljárását (ThreadWrapper), aminek paraméterben adja meg, hogy mit kértünk, és majd az hívja meg. Ez egy csúnya assembly rutin, mindenféle inicializációkat végez, számunkra most nem fontos. Ami fontos, hogy közben meghívja az általunk kívánt eljárást is.

Amire még érdemes figyelni, az a matrjóska rendszerű paraméterátadás. Gyakran előforduló eset, amikor kereteljárásokat írunk valamely funkció köré, hogy az egyetlen csatolható paraméterre nekünk is szükségünk volna, de ugyanakkor kívülről is kapunk egyet, amit továbbítani kell. Jelen esetben a Delphi nem a mi paraméterünket adta át, hanem készített egy kis csomagocskát, amibe beletette a majd valóban hivandó eljárást, és a mi paraméterünket. Majd ezt a csomagocskát (illetve arra mutató pointert) adta át a szállétrehozó API hívásnak. Később majd a ThreadWrapper a csomagból kiszedi, hogy mit kell hívni, és milyen paramétert kell neki továbbítani. A csomagocska felszabadítását is ronda ThreadWrapper fogja csinálni. De most még nem tartunk itt.

A szál életéről a továbbiakban nem tudunk semmit. Valamikor be fog indulni, de hogy az jövő héten lesz, vagy most azonnal, azt nem tudni.

Ezekután gyors ütemben az összes eljárás, amerre jöttünk, visszatér. A hívó fél folytatja a dolgát.

Mindettől függetlenül, valahol a számítógép másik zugában, életre kel egy szál. Ezen belül a windows kialakítja a körülményeket, majd elindítja az általunk (azaz a Delphi által) megadott eljárást, ami a ThreadWrapper. A ThreadWrapper előszedi a tényleg általunk (akarom mondani, a TThread objektum által) adott eljárást, és meghívja a szintén TThreadtől kapott paraméterrel. Így jutunk a ThreadProchoz.

function ThreadProc(Thread: TThread): Integer;
var
  FreeThread: Boolean;
begin
  try
    if not Thread.Terminated then
    try
      Thread.Execute;
    except
      Thread.FFatalException := AcquireExceptionObject;
    end;
  finally
    FreeThread := Thread.FFreeOnTerminate;
    Result := Thread.FReturnValue;
    Thread.FFinished := True;
    Thread.DoTerminate;
    if FreeThread then Thread.Free;
    EndThread(Result);
  end;
end;

Az eljárás paraméterben kapja meg azt az objektumot, ami őt létrehozta. Ezt ugye a TThread.Create adta át a BeginThreadnek, az továbbadta a CreateThreadnek becsomagolva, majd a ThreadWrapper megkapta, kicsomagolta, és most itt van. A fontos számunkra az, hogy meghívódik a TThread.Execute, itt jutottunk tehát el a szál objektum lényegéig. Az Execute tehát szintén a szálon fut, de ezen nem lepődtünk meg, hiszen ez az értelme neki.

A másik említendő a DoTerminate hívás. A DoTerminate hívja meg az OnTerminate eseménykezelőt, na de nem mindegy, hogy hogyan:

procedure TThread.DoTerminate;
begin
  if Assigned(FOnTerminate) then Synchronize(CallOnTerminate);
end;

A synchronize-ról majd később. Vonjuk le az első tanulságot:

1. A create metódus a hívó szálon fut, azaz leggyakrabban a fő szálon. Hiába tartozik a TThread objektumhoz.

2. Minden, amit az Execute-ból meghívunk, akár ha az az MyEdit.OnChange is, szálról fut.

Most nézzük azt a Synchronize-t. Ha simán CallOnTerminate szerepelne, akkor az az előbbiek értelmében azt jelentené, hogy szálról fog hívódni az OnTerminate, mintegy hattyúdalként. Hiszen az őt hívó ThreadProc is a szálon fut. Ezen változtat a Synchronize. A Synchronize ugyanis levelet küld a fő szálnak, hogy futtasson már le egy eljárást a kedvünkért. Ezután a szál guru meditációba süllyed. A fő szál, ha akad egy kis ideje, lefuttatja a kedvünkért az eljárást, majd válaszlevelet küld. A válaszlevél megérkeztével a szál feléled, és folytatja a munkáját, jelen esetben a megsemmisülést.

Ismét vigyázzunk! Teljesen mindegy, hogy az eljárás, amit átadtunk, hol van, kié, mié. Akár a TThread objektum egyik metódusát is átadhatjuk, az is a fő szálon fog futni. Mivel a kód, az kód, CPU utasítások sorozata, teljesen mindegy, hogy mely objektumé, mely unitban van, vagy mi a neve, de főleg attól, hogy mit szerettünk volna elérni.

További tanulságok tehát:

3. Az OnTerminate a fő szálon fut.

4. A Synchronize-nak átadott eljárás a fő szálon fut, akármi is az.

Házi feladat.

Adott egy szál objektum, a TWorkerThread. Ennek Execute metódusában az alábbi részletet látjuk:

...
  Synchronize(UpdateForm)
...

Az UpdateForm az alábbi műveletet végzi:

procedure TWorkerThread.UpdateForm;
begin
  FDisplayPanel.Caption := GetWorkProgressDesc;
end;

procedure TWorkerThread.GetWorkProgressDesc;
begin
  Result := PhaseStrings[FPhase] + ' ' + 
    FCurrentObject.Description;
end;

Kérdés: GetWorkProgressDesc függvény szálon fut vagy nem? A helyes válasz egy pontot ér, a helytelen válasz meg négy mázsa virgácsot.

Szorgalmi házi feladat profiknak. Szükséges ismeretek: message loop, ablakok és szálak viszonya.

Legyen a következő TThread implementációnk:

type
  TSomeThread = class(TThread)
  protected
    FReceiver: THandle;
    FStuffs: TThreadList;

    procedure Execute; override;
    procedure MessageHandler(var Msg: TMessage);
    procedure ProcessStuff(Stuff: pointer);
  public
    procedure AddStuff(Stuff: pointer);
    constructor Create;
    destructor Destroy; override;
  end;

constructor TSomeThread.Create;
begin
  FStuffs := TThreadList.Create;
  FStuffs.Duplicates := dupAccept;
  FReceiver := Classes.AllocateHwnd(MessageHandler);
  inherited Create(false);
end;

destructor TSomeThread.Destroy;
begin
  Terminate;
  PostMessage(FReceiver, WM_USER, 0, 0);
  inherited;
  DeallocateHWnd(FReceiver);
end;

procedure TSomeThread.AddStuff(Stuff: pointer);
begin
  FStuffs.Add(Stuff);
  PostMessage(FReceiver, WM_USER, 0, 0);
end;

procedure TSomeThread.Execute;
var
  LockedStuffs: TList;
  Stuff: pointer;
  Msg: TMsg;
begin
  while not Terminated do begin
    GetMessage(Msg, 0, 0, 0);
    DispatchMessage(Msg);

    repeat // checkpoint 1 - we never get here
      LockedStuffs := FStuffs.LockList;
      try
        if LockedStuffs.Count > 0
          then Stuff := LockedStuffs[0]
          else Stuff := nil;
      finally
        FStuffs.UnlockList;
      end;

      if Stuff = nil then break;

      ProcessStuff(Stuff);

      FStuffs.Remove(Stuff);
    until Terminated;
  end;
end;

procedure TSomeThread.MessageHandler(var Msg: TMessage);
begin
  // null
end; // checkpoint 2 - this runs fine

procedure TSomeThread.ProcessStuff(Stuff: pointer);
begin
  sleep(1000); // imitate work
end;

A dolog nem bonyolult, a FStuffs egy lista, amiben tennivalók vannak. Az AddStuff metódussal adhatunk az objektumnak Stuffokat, amiket az majd feldolgoz. Az AddStuff hozzáadja a Stuffot a listához, majd üzenetet küld egy dedikált üzenetfogadó ablaknak. Ez azért lesz hasznos, mert az Execute guru meditációba zuhan mindjárt az elején (GetMessage), és csak akkor ébred fel, ha üzenet jön. Ekkor megnézi, hogy valóban van-e tennivaló, ha van, azokat megcsinálja, majd ismét meditációba zuhan. Így a szál nem fogyaszt semmilyen erőforrást amikor éppen nincs mit tennie. Érdekesség, hogy magával az üzenettel nem kezdünk semmit, elég nekünk az a tény, hogy a GetMessage visszatért.

Tesztelés céljából dobjunk össze egy kis programot, hozzunk létre egy TSomeThread példányt, és akár ott rögtön, akár egy kis idő múlva, vagy akár gombnyomásra adjunk neki AddStuffal valami munkát.

A program nem működik, soha nem jut túl a GetMessage soron. Az a furcsa, hogy ha a checkpoint 2-vel jelölt sorra töréspontot teszünk, akkor a program ott szépen megáll, vagyis az üzenet megérkezik az ablaknak. Ennek ellenére a checkpoint 1-et a program soha nem éri el. Mi lehet az oka?


Üzenete jött!

Régi ígéretünknek teszünk eleget, és folytatjuk a megkezdett prímkiszámolós példaprogram sorozatot. A mai menü talán a legegyszerűbb módszer, ami persze tartalmaz csapdákat, hisz mi nem? Ő az Application.ProcessMessages.

Elöljáróban rövid védőbeszéd. Bizonyos fórumokon az a nézet terjedt el, hogy az A.PM egy ördögtől való, gonosz találmány, és sohasem szabad használni. Emellett azt az érvet szokás felhozni, hogy az AP.M hatására a program egyéb részei (eseménykezelők) végrehajtódhatnak, és megzavarhatják a szépen felépített munkakörnyezetet, akár újra "ráindíthatnak" magára a folyamatra (pl újra megnyomja a "kiszámít" gombot). Ez az érvelés természetesen hamis, ráadásul úgy hamis, hogy tulajdonképpen igaz, csak félrevezető. Ugyanis ez minden megoldásnak jellemzője, legyen az OnIdle, szál vagy A.PM. Nem véletlenül, hisz magából az igényből fakad, hiszen éppen az a célunk, hogy a program reagáljon, tehát kezelje le az üzeneteit. Az üzenet lehet paint, de éppúgy lehet egy gombnyomás is. Nem ússzuk meg tehát a felhasználói tevékenység korlátozását a művelet idejére, akármilyen megoldást is választunk.

A feladat tehát minden esetben az üzenetek rendszeres olvasása. Az A.PM esetén ezt mindenféle fantázia nélkül csináljuk: néha utasítjuk a programot, hogy olvassa el az üzeneteket, majd folytatjuk a munkát.

Az eredmény pedig itt van. A prímszám-alapprogramba felvettünk két gombot, egy start és egy stop gombot. Erre azért van szükség, mert az OnIdle megoldásnál ismertetett azonnali végrehajtás ebben az esetben nehezebben megvalósítható, és mi most az egyszerűségre törekszünk. Tehát maga a számítás a gomb eseményében van, egész pontosan onnan hívódik.

Először letiltjuk az összes nemkívánatos gombot, egyebet, valamint engedélyezzük a stop gombot.


procedure TMainForm.EnableControls(Enable: boolean);
begin
  NumberEdit.ReadOnly := not Enable;
  CalcButton.Enabled := Enable;
  StopButton.Enabled := not Enable;
end;

Maga a számítás pedig nagyon hasonló az OnIdle megoldáshoz, csak itt nem hagyjuk abba a műveletet egy részlet után, hanem egyvégtében megcsináljuk. Közben az ott ismertetett módon számáló és GetTickCount segítségével időzítve néha megnézzük az üzeneteket.


  Aborted := false;
  Done := false;
  CurrentDivisor := 1;
  i := 0;
  LastTime := GetTickCount;
  repeat
    CurrentDivisor := CurrentDivisor + 1;
    Done := (TheNumber mod CurrentDivisor = 0) or (CurrentDivisor > MaxDivisor);
    i := (i + 1) and $3FFF;

    if (i = 0) and (GetTickCount - LastTime > 50) then begin
      ResultLabel.Caption := 'is still being tested, ' + IntToStr(CurrentDivisor);
      LastTime := GetTickCount;
      Application.ProcessMessages;
    end;
  until Aborted or Done;

Az Aborted változó kezdetben false. Az A.PM lehetővé teszi, hogy a stop gomb megkapja a lenyomásról szóló üzenetet, így az OnClick eseménye meghívódhat, hogy az Aborted-et true-ra állítsa.


procedure TMainForm.StopButtonClick(Sender: TObject);
begin
  Aborted := true;
end;

A tényleges végrehajtási sorrend valami ilyen:


   ...
   ResultLabel.Caption := 'is still being tested, ' + IntToStr(CurrentDivisor);
   LastTime := GetTickCount;
   Application.ProcessMessages;

     ... barangolás a delhi VCL dzsungelében

     ... win32 PeekMessage / DispatchMessage

     ... barangolás a delhi VCL dzsungelében

     ... StopButtonClick meghívása

     Aborted := true;
   
     ... StopButtonClick vége, ismét barangolás sokfelé

     ... végül folytatódik a TestIfPrime

   end;
   until Aborted or Done;
   ...

Ha ezt a hurkos futási sorrendet megértjük, és el tudjuk képzelni az eseménykezelőinket az A.PM után beszúrva, soha nem érhet meglepetés. Szükséges a nemkívánatos GUI elemeket letiltogatni, vagy az eseménykezelőiket úgy megírni, hogy figyelemmel legyenek a folyamatban levő számításra. Ha a form túlságosan tele van mindenfélével, kézenfekvő megoldás egy "kérem várjon" ablakot mutatni folyamat alatt.

Ez tehát az Application.ProcessMessages megoldás. Nem nagy ügy, kényelmes, és hatékony. Azért legközelebb mutatunk szálas megoldást is.


Idle work

Korábbi ígéretünkhöz híven egyszerű példán bemutatjuk az OnIdle használatát megszakítható hosszú tevékenységek megvalósításához.

A hajánál fogva előrángatott példa pedig az lesz, hogy a felhasználó által megadott számról döntsük el, hogy prímszám-e, vagy nem. Mivel rendesek vagyunk, ha nem prímszám, egy prímtényezőt is kiírunk. Valójában nem mert rendesek vagyunk, hanem mert az algoritmus a jó öreg szisztematikus kimerítés lesz, és az konstruktív viselkedésű, egyből egy prímtényezőt is megad.

A továbbiak egyszerüsítése végett készítettem egy kis mintaprogramot, ami a többféle megoldás közös alapja lesz. Ez letölthető zip formában itt, bár nem érdemes. A program egy formot tartalmaz, azon egy beviteli mező, ide írhatja a felhasználó a tesztelendő számot. Található rajta egy cimke, ebben fogjuk értesíteni a felhasználót a folyamat állásáról vagy végeredményéről. A gomb eseményében e programvázban csak ellenőrzés található, azaz a számot átkonvertáljuk számmá, és ha nem sikerül, erről tájékoztatást adunk.

Az OnIdle alapú megvalósításhoz a tennivalót fejben át kell értelmezni, nem egy folyamatként kell látni, hanem egy útként, amin éppen tartunk valahol és valamerre. A programozó dolga felderíteni, hogy miként lehetséges ábrázolni a hol és merre adatokat, hogy bármikor felkaphassuk a fonalat. Valamint azt is el kell dönteni, hogy egy szuszra mennyit haladjunk. A folyamat végrehajtásának tehát van egy állapota. Ezt az állapotot jelen esetben az alábbi módon ábrázoljuk:


  private
    TheNumber: Int64;
    CurrentDivisor: Int64;
    Running: boolean;
A TheNumber változóban fogjuk tárolni a megadott számot. A CurrentDivisor tartalmazza a legutóbb tesztelt osztót. A program innen folytatja majd a nagyobb osztók felé az ellenőrzést. A számítás kezdetén ez egy, ami ugyan nem volt tesztelve, de így az első valóban tesztelt osztó a kettő lesz. A Running változó elkerülhető lett volna, ha a CurrentDivisor valamely érvénytelen értékével (pl nulla) jelezzük azt, hogy nincs tennivaló. De a jobb érthetőség kedvéért egy boolean változó helyfoglalása igazán jutányos ár.

A program inicializációjában feliratkozunk az Application.OnIdle eseményre.


procedure TMainForm.FormCreate(Sender: TObject);
begin
  Application.OnIdle := AppIdle;
end;
Ha a felhasználó megváltoztatja a beírt számot, ennek megfelelően változtatjuk a feladat állapotát. Az eseménykezelőben a try..except között részt az alábbi módon készítjük el.


    TheNumber := StrToInt64(NumberEdit.Text);
    if TheNumber < 2 then begin
      ResultLabel.Caption := 'is too tiny';
      Running := false;
    end else begin
      CurrentDivisor := 1;
      Running := true;
      ResultLabel.Caption := 'is still being tested';
    end;
Ha a beírt szám megfelelő, a Running beállításával engedélyezzük a futást, és a felhasználót tájékoztatjuk, hogy a kért feladaton dolgozunk. A többi az OnIdle dolga.


procedure TMainForm.AppIdle(Sender: TObject; var Done: Boolean);
var
  StartTime: cardinal;
  i: cardinal;
  MaxDivisor: Int64;
begin
  if Running then begin
    StartTime := GetTickCount;
    i := 0;
    MaxDivisor := Trunc(Sqrt(0.0+TheNumber));
    repeat
      CurrentDivisor := CurrentDivisor + 1;
      Done := (TheNumber mod CurrentDivisor = 0) or
        (CurrentDivisor > MaxDivisor);
      i := (i + 1) and $3FFF;
    until Done or ((i = 0) and (GetTickCount - StartTime > 50));

    if Done then begin
      if MaxDivisor < CurrentDivisor then
        ResultLabel.Caption := 'is prime'
      else
        ResultLabel.Caption := 'is not a prime, a divisor is ' +
          IntToStr(CurrentDivisor);
    end else begin
      ResultLabel.Caption := 'is still being tested, ' +
        IntToStr(CurrentDivisor);
    end;

    Running := not Done;
  end else begin
    Done := true;
  end;
end;
Az algoritmus nem szorul sok magyarázatra, a szám gyökéig ellenőrizzük a potenciális osztókat, ha osztható, kész vagyunk, ha elértük a maximumot, akkor nincs osztó, és kész vagyunk. Ami érdekesebb, az az i változó szerepe, valamint a GetTickCount használata. Az ilyen egyszerű feladatra valószínűleg túlzottan összetett megoldást állatorvosi lónak szántuk. Hogy a programunk élő maradjon, de egyben optimumközeli számítási teljesítménye is legyen, se túl sokszor, se túl ritkán nem szabad a munkát megszakítani. Önkényesen kijelölhetünk két elvárható értéket, a minimális hasznos processzoridőt, és a program maximális reakcióidejét. Kézenfekvő a processzorkihasználtságot 99-99,9% környékén kijelölni, a minimális válaszadási időt pedig 10-100ms között.

Hogy mennyi ideig tart egy üres PeekMessage hívás, azt csak találgatni lehet, de ha például tízmillió óraciklusnyi számolásra jut egy PeekMessage, joggal gondolhatjuk, hogy annak aránya jócskán 1% alatt lesz. Tízmillió óraciklus GHz órafrekvenciás CPU esetén miliszekundum időtartományba esik, tehát van kényelmes mozgásterünk.

A fenti programrészlet a túlbonyolítás módszerét használja. Célunk, hogy 50ms legyen az egyvégtében megcsinált számítás. Erre a Windows által biztosított 10-20ms pontosságú számlálót használjuk fel, amit a GetTickCount API hívással szerezhetünk meg. Feltételezhetjük, hogy a GetTickCount igen gyors, de mégis lassú a prímteszt ciklusmagjában található kódhoz hasonlítva (modulo, összehasonlítások, összeadás és bináris és). Ezért itt egy trükköt alkalmazunk, egy párhuzamosan modulo-növelünk egy változót, és csak ha az nulla, akkor ellenőrizzük, hogy eltelt-e a tervezett idő. A modulo nagyságát úgy határozzuk meg, hogy a GetTickCount várhatóan elenyésző legyen a számításhoz képest. Ezt az értéket most találomra $4000-ben határoztuk meg, ami decimális 16384, és azt gondoljuk, hogy ennyi osztáshoz képest egy egész érték visszahozása nem lehet számottevő. A szám választásakor egy bináris kerek számot választottunk, hogy a modulo számítása bináris és művelettel elvégezhető legyen, ami sokkal gyorsabb, mint a modulo.

A ciklusból ki kell lépni, ha osztót találtunk, elértük a maximum osztót, vagy lejárt az idő. Előbbi két esetben a Done értéke true lesz, az időtúllépéskor false. Mindhárom esetben a felhasználót tájékoztatjuk a feladat állapotáról.

A végeredmény pedig letölthető itt


Kérem, várjon!

Vannak műveletek, amik sokáig tartanak. Programozási szempontból az ilyen műveletekkel több probléma is van. A legelső, hogy amig a művelet tart, a programunk felülete "döglött", mivel nem olvassa az üzeneteket, amit a windows küld neki. A másik probléma, hogy a felhasználó esetleg meggondolhatja magát, főleg ha véletlenül indította el a műveletet. Nem illik azzal büntetni, hogy végig kell várnia a nemkívánt folyamatot. Adni kell tehát valami lehetőséget a megszakításra, és ez visszavezet az előző problémához, hiszen a felhasználó tevékenységéről is üzenetek formájában értesülünk.

Kérdezz csak rá bármely fórumon, rögtön kapod a választ: használj szálakat. Elhamarkodott válasz.

Javítás: Delphiben három módszer is van, esetleg több, de most háromról lesz szó, és ezek közül a szál a legbonyolultabb, legveszélyesebb és legköltségesebb, miközben előnyei általában elenyészők, vagy nincsenek is. Ez a három:

  1. szál
  2. Application.ProcessMessages
  3. Application.OnIdle

Szál használata a legegyszerűbb, abban az értelemben, hogy a feladatot nem kell "beágyazni" a program szokásos folyamataiba (message pump, eseményekre reagálás). A külön szál egyben külön logikai szálat is jelent. Hátránya viszont, hogy semmilyen változót, objektumot, ablakot, stb, ami a fő szál tulajdona, a szál nem érhet el szinkronizáció nélkül. Legjobb példa erre az, hogy egy egyszerű string típusú globális változó egyidejű olvasása teljes összegabalyodáshoz vezethet, legalábbis D6-ig ez volt a helyzet (későbbi delphim most nincs kéznél). (Korrekció: időközben megtudtam, hogy ez nem igaz, D5-ben javítva lett, azóta a string szálbiztos.)

Az Application.ProcessMessages hivogatása a legtermészetesebb megoldása a feladatnak, hiszen éppen ez volt az eredeti célkitűzés. Ezzel egy probléma van, az, hogy hogyan illeszthető be a folyamatba a PM rendszeres, de nem túl sűrű hivogatása. Ha a program nem olvassa maximum 100 ms-onként az üzeneteit, akkor nem lesz kényelmesen "élő", akadozni fog. Ez legtöbbször megvalósítható, ám vannak olyan esetek, amikor egy elemi művelet hosszú másodpercekig tart. Ilyen esetben más megoldás után kell nézni. Túl gyakori hívás sem előnyös, ekkor ugyanis túl sok időt pazarolunk az üzenetek olvasgatására, ami a sebesség rovására megy. Gyakori ellenérv a PM ellen, hogy a program reentráns lesz, azaz a gombok élnek, a programból ki lehet lépni, stb, ami a folyamatra hatással lehet, és ezt figyelembe kell venni. Csakhogy ugyanez a szálas megoldásra is igaz, tehát a programozó nem ússza meg, hogy a nemkívánatos GUI elemeket letiltsa a művelet idejére.

Az Application.OnIdle egy méltatlanul hanyagolt lehetőség. Kétségtelenül ezzel van a legbonyolultabb dolgunk, a folyamatot ugyanis rövid időszeletekre kell osztanunk, és a mindenkori állapotot elmenteni, hogy legközebb onnan folytathassuk, ahol abbahagytuk. Lévén a legkevésbé ismert módszer, néhány szóval bemutatjuk a lényegét.

A message loop delphiben úgy néz ki, hogy egy ciklus addig hivogatja a PeekMessage API függvényt, amig az nem jelenti, hogy nincs több üzenet. Ekkor meghívja az OnIdle eventet. Ha az onIdle Done paramétere a visszatérés után false, akkor ismét PeekMessage-ek következnek, majd megint OnIdle. Amint az OnIdle Done=true-val tér vissza, egy WaitMessage hívás következik, és aztán minden kezdődik előről

Az OnIdle eseménykezelőbe nem szabad hosszú tevékenységet tenni, mivel ellenkező esetben az alkalmazás akadozni fog. Tehát a hosszas műveletünket fel kell bontani rövid részműveletekre, egy OnIdle hívás során max 100ms-nyi részműveletet végzünk el. Ha ezzel nem értünk a teljes feladat végére, a Done paramétert false-ra kell állítani. Ha például a feladat az, hogy egy DataSet sorain menjünk végig, és mindegyikkel tegyünk valamit, az OnIdle eseményben feldolgozunk mondjuk száz sort, és ha eközben Eof=true helyzet áll elő, akkor a Done true-ra állíttatik. Ebben a példában a DataSet pozíciója szolgál a feladat állapotának mentésére. Ez a programfelépítés egyébként nagyon megkönnyíti a többféle háttérfeladat egyidejű kezelését, ilyen esetben az OnIdle prioritás sorrendjében válogat a feladatok közül. Valamint megkönnyíti a feladat újrakezdését is, ha a kondíciók változása ezt igényli, ugyanis egyszerűen az elmentett státuszt módosítjuk, és a legközelebbi OnIdle már az új állapotnak megfelelően dolgozik majd.

Hamarosan egy hajánál fogva előrángatott példán bemutatjuk a három megközelítés összehasonlító kivitelezését.


balinto 2006