Example of multi-threaded application: array of threads/pl

From Free Pascal wiki
Revision as of 14:24, 23 September 2023 by Slawek (talk | contribs) (→‎3. Tworzenie głównego programu.: poprawka)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

English (en) español (es) 日本語 (ja) polski (pl)

Przykład aplikacji wielowątkowej: tablica wątków

Tutaj chcę pokazać przykład, jak utworzyć wiele wątków i poczekać, aż zakończą swoje zadania (nie potrzebuję żadnej synchronizacji). Piszę ten tutorial, ponieważ po przeczytaniu [[Multithreaded Application Tutorial/pl|Samouczek aplikacji wielowątkowej] nie było dla mnie oczywiste napisanie takiego programu. Pisałem aplikację dla systemu macOS, ale powstały kod powinien działać na każdym systemie.

Załóżmy, że mamy następującą pętlę:

...
for i:=1 to n do begin
  power(i,0.5)
end;
...

Ta pętla wykonuje n razy potęgowanie iteratora ze stopniem 0,5 (co odpowiada pierwiastkowaniu 2 stopnia). Użyjmy wątków, aby osiągnąć to samo zadanie równolegle.

Czy potrzebujemy wielu wątków?

W nowoczesnych komputerach z wielordzeniowymi procesorowymi, użycie wielu wątków może radykalnie zwiększyć wydajność. Jednak nowoczesne komputery są bardzo szybkie, a kod wielowątkowy jest często trudniejszy do debugowania i konserwacji niż kod jednowątkowy. Należy rozważyć, czy zaoszczędzony czas przetwarzania uzasadnia złożoność programowania wielowątkowego i czy algorytm dobrze nadaje się do obliczeń równoległych. Na koniec należy zauważyć, że niektóre algorytmy, które mogą czerpać korzyści z obliczeń równoległych, mogą skorzystać na wykorzystaniu wątków procesora (jak pokazano tutaj), podczas gdy inne mogą być bardziej dostosowane do procesora graficznego (gdzie narzędzia takie jak OpenCL mogą być bardziej optymalne).

Zarządzanie pamięcią

Ponieważ wiele wątków będzie działać jednocześnie, należy upewnić się, że nie występują w nich problemy z rywalizacją o pamięć. Jeśli wiele wątków zapisuje dane w tych samych lokalizacjach w pamięci, wystąpią problemy z rywalizacją o pamięć. Niektóre algorytmy nie nadają się do wielowątkowości, ponieważ każde obliczenie zależy od wcześniejszych wyników. Z drugiej strony wielowątkowość działa bardzo efektywnie w przypadku problemów, w których obliczenia można wykonywać niezależnie i równolegle. W tym przykładzie rozwiążemy problem, który jest całkowicie niezależny i dlatego łatwy do zrealizowania wieloma wątkami. Zaawansowane algorytmy będą musiały korzystać z funkcji blokowania pamięci, aby uniknąć rywalizacji.

W naszym przykładzie każdy wątek będzie zapisywał w różnych lokalizacjach pamięci. W szczególności utworzymy tablicę 1..n i obliczymy wartość power(i,0.5) (potegi), gdzie i należy do zakresu 1..n. Każdy wątek otrzyma niezależną część zakresu do obliczenia. Rozważ n=1000. Jeśli użyjemy jednego wątku, będzie on miał do wykonania zadanie z całego zakresu 1..1000, natomiast jeśli zastosujemy dwa wątki, jeden zajmie się zakresem 1..500, a drugi 501..1000. W ten sposób wątki będą pracować nad wypełnieniem różnych części naszej tablicy pamięci.

1. Wykryj liczbę dostępnych rdzeni.

Komputer posiadający tylko jeden rdzeń nie odniesie korzyści z obsługi wątków, podczas gdy komputer z czterema fizycznymi rdzeniami, każdy z funkcją hyperthreading (będący w stanie wykonywać dwa zadania jednocześnie), będzie w stanie przetwarzać do ośmiu zadań jednocześnie. Niżej pokazany moduł „cpucount” podaje liczbę dostępnych rdzeni. Możesz użyć tej opcji, aby określić, z ilu wątków powinien korzystać Twój program na danym komputerze. W przypadku komputerów z czterema lub więcej rdzeniami wiele osób chce uruchamiać wątki n-1 (gdzie n to liczba rdzeni), rezerwując jeden rdzeń na potrzeby interfejsu graficznego i innych zadań, ponieważ różnica w wydajności między wątkami n i n-1 nie będzie istotna z tak dużą liczbą rdzeni.


Light bulb  Uwaga: Definicja funkcji użyta poniżej dla FPsysctl() dotyczy FPC v3.0.4 - Dla FPC v3.2.0 i v3.3.1 (trunk) pierwszym argumentem nie jest już pchar, ale pcint. Dostosuj odpowiednio przykładowy kod.
unit cpucount;

{$mode objfpc}{$H+}

interface
//zwraca liczbę rdzeni: komputer z dwoma rdzeniami hiperwątkowymi zgłosi 4
function GetLogicalCpuCount: Integer;

implementation

{$IF defined(windows)}
uses windows;
{$endif}

{$IF defined(darwin)}
uses ctypes, sysctl;
{$endif} 

{$IFDEF Linux}
uses ctypes;

const _SC_NPROCESSORS_ONLN = 83;
function sysconf(i: cint): clong; cdecl; external name 'sysconf';
{$ENDIF}


function GetLogicalCpuCount: integer;
// zwraca dobrą domyślną liczbę wątków w tym systemie
{$IF defined(windows)}
//zwraca całkowitą liczbę procesorów dostępnych w systemie, w tym logiczne procesory hiperwątkowe
var
  i: Integer;
  ProcessAffinityMask, SystemAffinityMask: DWORD_PTR;
  Mask: DWORD;
  SystemInfo: SYSTEM_INFO;
begin
  if GetProcessAffinityMask(GetCurrentProcess, ProcessAffinityMask, SystemAffinityMask)
  then begin
    Result := 0;
    for i := 0 to 31 do begin
      Mask := DWord(1) shl i;
      if (ProcessAffinityMask and Mask)<>0 then
        inc(Result);
    end;
  end else begin
    //nie można uzyskać maski koligacji, więc po prostu raportujemy całkowitą liczbę procesorów
    GetSystemInfo(SystemInfo);
    Result := SystemInfo.dwNumberOfProcessors;
  end;
end;
{$ELSEIF defined(UNTESTEDsolaris)}
  begin
    t = sysconf(_SC_NPROC_ONLN);
  end;
{$ELSEIF defined(freebsd) or defined(darwin)}
var
  mib: array[0..1] of cint;
  len: cint;
  status: integer;
begin
  mib[0] := CTL_HW;
  mib[1] := HW_NCPU;
  len := sizeof(Result);
  status := fpsysctl(pchar(@mib), Length(mib), @Result, @len, Nil, 0);
  if status <> 0 then WriteLn('Error in fpsysctl()');
end;
{$ELSEIF defined(linux)}
  begin
    Result:=sysconf(_SC_NPROCESSORS_ONLN);
  end;

{$ELSE}
  begin
    Result:=1;
  end;
{$ENDIF}
end.

2. Utwórz niestandardową klasę wątków.

Do definiowania nowych wątków używam osobnego modułu. Zauważ, że ustawiam „FreeOnTerminate” na false - więc mój program będzie musiał pozbyć się każdego wątku, gdy się zakończy. Ułatwia to żonglowanie wieloma wątkami (jeśli ustawisz FreeOnTerminate na true i uruchomisz wiele bardzo szybkich zadań, możliwe jest, że wątek zostanie zwolniony, zanim program sprawdzi, czy wątek jest ukończony – a sprawdzenie nieistniejącego wątku może spowodować wyjątek). Ustawiając FreeOnTerminate na false, mogę mieć pewność, że każdy wątek zakończył się pomyślnie.

Dobrze zachowujący się wątek w pętli powinien regularnie sprawdzać zakończenie, aby w przypadku, gdy procesy zewnętrzne (np. Destroy) chciały przerwać wykonywanie, nie musiały czekać wiecznie. Oryginalny kod tego nie robił, więc nie można było ukończyć zwolnienia pamięci (free), dopóki wszystkie obliczenia nie zostały zakończone.

Kontrola podstawowego kodu pokazuje, że po wyjściu z polecenia „Execute” wywoływana jest funkcja OnTerminate, ale opcja Terminated nie jest ustawiona. Ponieważ w przykładzie nie używa się już funkcji waitfor, ale po prostu sprawdza się Terminated, to dodałem Terminate po zakończeniu pętli.

unit mythreads;
{$mode objfpc}{$H+}
interface
uses
  Classes, SysUtils, Math;
type
  TData = array of double;
  PData = ^TData;
 Type
    TMyThread = class(TThread)
    private
    protected
      tPtr: PData;
      tstart,tfinish: integer;
      procedure Execute; override;
    public
      property Terminated;
      Constructor Create(lstart, lfinish: integer; var lPtr: PData);
    end;

implementation

  constructor TMyThread.Create(lstart, lfinish: integer; var lPtr: PData);
  begin
    FreeOnTerminate := False;
    tstart := lstart;
    tfinish := lfinish;
    tPtr := lPtr;
    inherited Create(false);
  end;
  procedure TMyThread.Execute;
  var
    i: integer;
  begin
    i:= tstart;
    While not Terminated and (i<= tfinish) do 
    //Dobrze zachowujący się wątek w pętli powinien regularnie sprawdzać zakończenie
        begin
    //for i := tstart to tfinish do
        tPtr^[i] := power(i,0.5);
        inc(i);
        end;
    Terminate;
  end;

end.

3. Tworzenie głównego programu.

Jeśli korzystasz z systemu Linux/Unix, musisz dodać moduł 'cthreads' do modułu głównego 'main', a nie do modułu z wątkami! Obecnie Lazarus domyślnie dodaje moduły warunkowo:

 {$IFDEF UNIX}
 cthreads,
 {$ENDIF}
 {$IFDEF HASAMIGA}
 athreads,
 {$ENDIF}

więc w takiej sytuacji nie musisz tego już robić. W przypadku MS Windows nie jest to potrzebne.

Należy pamiętać, że istnieją dwa sposoby sprawdzenia, czy wszystkie wątki zostały zakończone. Możesz użyć wbudowanej funkcji „waitFor” - działa to bardzo dobrze, ale na moim komputerze Mac zauważyłem, że odświeża się tylko co 100 ms. Jest to idealne rozwiązanie dla rzeczywistych programów (używamy wątków tylko w przypadku problemów obliczeniowo wolnych) i zmniejsza obciążenie wątku. Jednak w przypadku szybkich przykładowych testów porównawczych może ukryć zalety wielowątkowości (ponieważ operacje wymagają minimum 100 ms niezależnie od liczby wątków). Dlatego w tym przykładzie wykrywam status zakończenia wątku co 2 ms. Zapewnia to dokładniejszy czas analizy porównawczej.

Pamiętaj, aby zwolnić każdy wątek, gdy już z nim skończysz. Ponieważ ustawiliśmy „FreeOnTerminate := False”, program musi to zrobić jawnie.

Wskazówka: w moim środowisku IDE Lazarusa nie mogłem debugować aplikacji wielowątkowych, jeśli nie korzystałem z 'pthreads'. Czytałem, że jeśli użyjesz 'cmem', program będzie działał szybciej, ale zdecydowanie zalecam sprawdzenie tego pod kątem konkretnego przypadku (mój program zawiesza się, gdy używam 'cmem').

uses //    cmem,pthreads,
  cthreads, Classes, SysUtils, CustApp, MyThreads, Math, cpucount;

procedure DoUnThreaded (nValues: integer);
var
 dataArray: TData;
 i: integer;
 StartMS: double;
begin
     if (nValues < 1) then exit;
     StartMS:=timestamptomsecs(datetimetotimestamp(now));
     setlength(dataArray, nValues+1);//+1 since indexed 0..n-1
     for i:=1 to nValues do
         dataArray[i] := power(i,0.5);  ;
     Writeln('Przetworzono szeregowo '+inttostr(nValues)+' wartości w czasie '+floattostr(timestamptomsecs(datetimetotimestamp(now))-StartMS)+'ms, wykonując '+inttostr(nValues)+'^0.5 = '+floattostr(dataArray[nValues]));
end;

procedure DoThreading (nThreadsIn, nValues: integer);
var
 threadArray: array  of TMyThread;
 dataArray: TData;
 lData : PData;
 nThreads, i,lStart,lFinish: integer;
 StartMS: double;
begin
     if (nThreadsIn < 1) or (nValues < 1) then exit;
     nThreads := nThreadsIn;
     if  nThreads > nValues then nThreads := nValues;
     StartMS:=timestamptomsecs(datetimetotimestamp(now));
     setlength(threadArray,nThreads+1);//+1 do indeksowania 0..n-1
     setlength(dataArray, nValues+1);//+1 do indeksowania 0..n-1
     lData := @dataArray;
     lStart := 1;
     for i:=1 to nThreads do begin
         if i < nThreads then
            lFinish:=i*(nValues div nThreads)
         else
             lFinish:=  nValues;
         threadArray[i]:= TMyThread.Create(lStart, lFinish, lData);
         //Writeln('Przetwarzanie '+inttostr(i)+' wątkowe zakresu '+inttostr(lStart)+'..'+inttostr(lFinish));
         lStart := lFinish+1;
     end;
     //for i:=1 to nThreads do if not ThreadArray[i].Terminated then Sleep(2); //Logicznie źle, bo wprowadza opóźnienie n * 2ms
     //for i:=1 to nThreads do threadArray[i].waitFor;  //wygląda na to, że ma macOS usypia się na 100ms

     for i:=1 to nThreads do 
        While not ThreadArray[i].Terminated do Sleep(2);   //nie kontynuuj, dopóki wszystkie wątki nie zostaną zakończone
 
     for i:=1 to nThreads do threadArray[i].Free; //Free nie zostanie ukończone do czasu zakończenia procesu wykonywania wątku.
     Writeln('Przetworzono '+inttostr(nThreads)+' wątkowo '+inttostr(nValues)+' wartości w czasie '+floattostr(timestamptomsecs(datetimetotimestamp(now))-StartMS)+'ms, wykonując '+inttostr(nValues)+'^0.5 = '+floattostr(dataArray[nValues]));
end;

begin
  Writeln('Komputer zgłasza '+inttostr(GetLogicalCpuCount)+' rdzeni: to prawdopodobnie jest optymalna liczba wątków');
  DoUnthreaded(10);
  DoThreading(1,10);
  DoThreading(2,10);
  DoThreading(4,10);
  DoThreading(8,10);
  DoUnthreaded(100000000);
  DoThreading(1,100000000);
  DoThreading(2,100000000);
  DoThreading(4,100000000);
  DoThreading(8,100000000);
end.

Wyniki.

Wyniki pokazują, że występuje opóźnienie w tworzeniu wątków, ale w przypadku dużych zadań wiele równoległych wątków przewyższa to przetwarzanie szeregowe. Należy pamiętać, że gdy liczba wątków przekracza liczbę rdzeni (4 w tym komputerze), dodatkowe wątki nie przynoszą żadnych korzyści. Nie należy oczekiwać, że prędkość będzie idealnie skalować się wraz z liczbą wątków: wielowątkowość wiąże się z pewnym obciążeniem, a większość nowoczesnych procesorów będzie działać nieco szybciej, gdy wykonywane jest tylko jedno intensywne zadanie, niż podczas wykonywania uruchomionych zadań obciążających wiele rdzeni ('turboboost').

  • Komputer zgłasza 4 rdzeni: to prawdopodobnie jest optymalna liczba wątków
  • Przetworzono szeregowo 10 wartości w czasie 0ms, wykonując 10^0.5 = 3.16227766016838
  • Przetworzono 1 wątkowo 10 wartości w czasie 3ms, wykonując 10^0.5 = 3.16227766016838
  • Przetworzono 2 wątkowo 10 wartości w czasie 5ms, wykonując 10^0.5 = 3.16227766016838
  • Przetworzono 4 wątkowo 10 wartości w czasie 9ms, wykonując 10^0.5 = 3.16227766016838
  • Przetworzono 8 wątkowo 10 wartości w czasie 19ms, wykonując 10^0.5 = 3.16227766016838
  • Przetworzono szeregowo 100000000 wartości w czasie 10214ms, wykonując 100000000^0.5 = 10000
  • Przetworzono 1 wątkowo 100000000 wartości w czasie 10320ms, wykonując 100000000^0.5 = 10000
  • Przetworzono 2 wątkowo 100000000 wartości w czasie 5894ms, wykonując 100000000^0.5 = 10000
  • Przetworzono 4 wątkowo 100000000 wartości w czasie 3801ms, wykonując 100000000^0.5 = 10000
  • Przetworzono 8 wątkowo 100000000 wartości w czasie 3733ms, wykonując 100000000^0.5 = 10000