Obliczenia współbieżne na wątkach

Program pokazuje jak zrealizować obliczenia współbieżne na wątkach.

Kiedy można stosować obliczenia współbieżne?

Aby obliczenia współbieżne były możliwe do zastosowania, musi istnieć możliwość podzielenia zadania
obliczeniowego na niezależne części.

Mój przykład

W moim przykładzie używam czterech wątków do obliczenia silni. Obliczanie silni w ten sposób nie ma
większego sensu i służy tu prostemu zobrazowaniu, na czym rzecz polega.
Omówię to na przykładzie silni z 12.
Silnia z N to iloczyn liczb od 1 do N. (Silnia z 0 jest 1.)
Dla 12 jest to 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 10 * 11 * 12.
Łatwo zauważyć, że taki iloczyn można podzielić na iloczyny częściowe np.:
(1 * 2 * 3) * (4 * 5 * 6) * (7 * 8 * 9) * (10 * 11 * 12).
Celowo utworzyłem 4 iloczyny częściowe, oddzielone nawiasami, bo mam zamiar przydzielić każdy z nich
jednemu z czterech wątków, którymi się posługuję w przykładzie.
Na koniec pomnożę iloczyny częściowe przez siebie i otrzymam wynik.

Synchronizacja różnych wątków

Warto zauważyć, że to końcowe mnożenie zostanie również wykonane przy użyciu wątków.
Ponieważ nie wiadomo, w jakim momencie poszczególne wątki wyliczą iloczyny częściowe, należy
wprowadzić synchronizację wątków. Wygląda to tak:
Pierwszy wątek pobiera własny, już obliczony iloczyn częściowy i ustawia flagę gotowości tego
iloczynu do pobrania przez inny wątek.
Drugi wątek po obliczeniu własnego iloczynu czeka na pojawienie się flagi gotowości pierwszego wątku
i mnoży własny iloczyn przez iloczyn z pierwszego wątku, po czym również ustawia flagę gotowości do
pobrania iloczynu przez trzeci wątek. Itd., co można zapisać jako:
Wątek 1: iloczyn_końcowy_1 = iloczyn_częściowy_1
Wątek 2: iloczyn_końcowy_2 = iloczyn_końcowy_1 * iloczyn_częściowy_2
Wątek 3: iloczyn_końcowy_3 = iloczyn_końcowy_2 * iloczyn_częściowy_3
Wątek 4: iloczyn_końcowy_4 = iloczyn_końcowy_3 * iloczyn_częściowy_4
Dzięki synchronizacji przez flagi odbędzie się to w takiej, a nie innej kolejności.
Wynikiem obliczeń jest iloczyn_końcowy_4.

Losowość

Abyś mógł się przekonać, że kolejność zadań wykonywanych przez wątki jest losowa (za wyjątkiem
przypadku synchronizacji) dodałem przycisk ‘Kolejność’.
Praktycznie za każdym razem jest inna.

Przypadek ogólny

Dla liczb, które nie dzielą się przez 4 (ilość wątków) np. 13, ostatni iloczyn jest rozszerzany.
Dla 13 będzie on wyglądał (10 * 11 * 12 * 13). Jest to prosta metoda, ale można się pokusić o
przerobienie programu tak, aby np. dla 14 utworzył iloczyny:
(1 * 2 * 3 * 13) * (4 * 5 * 6 * 14) * (7 * 8 * 9) * (10 * 11 * 12),
aby możliwie równomiernie obciążyć wątki.
Dla liczb zbyt małych, aby „obsadzić” wszystkie wątki, należy przyjąć iloczyny częściowe równe 1.
(Gdybyśmy dodawali wyrazy a nie mnożyli, byłoby to 0).

Uruchomienie programu

Aby uruchomić program, należy utworzyć nowy projekt z Form1, Unit1 i wkleić całą zawartość mojego
Unit1 oraz powiązać w zdarzenia OnCreate i OnClose w kodzie z formą.
Wszystkie kontrolki zostaną utworzone automatycznie po uruchomieniu.

Wykonanie obliczeń

W żółte pole należy wpisać liczbę od 0 do 20, której silnię chcemy obliczyć.
Wynik po kliknięciu Oblicz pojawi się w zielonym polu.

unit Unit1;

interface

uses
  Windows, SysUtils, Classes, Forms, Graphics, StdCtrls, ExtCtrls, Controls;

const
  ThreadCount = 4; //ilość wątków
  JobCount    = 4; //ilość zadań w wątku

type
  TThreadCalc = class(TThread)
  private
    Tag: integer; //numer wątku
    Job: integer; //numer zadania
    Succ: array [0..JobCount - 1] of integer; //kolejność wykonywania zadań
                                              //przez wszystkie wątki
    FGetSucc: boolean;  //jeśli true, badana jest kolejność
  protected
    procedure Execute; override;
    procedure GetData;  //pobranie danej do obliczenia silni
    procedure PutData;  //wyświtlenie rezultatu
    procedure CalcProc; //procedura obliczeniowa
    procedure GetSucc;  //bada kolejność
  public
    constructor CreateWithPar(aTag: integer; aFGetSucc: boolean = false);
      //konstruktor z parametrami
  end;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
    procedure Calc(Sender: TObject);
    procedure TimerTimer(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
  public
    Threads: array [0..ThreadCount - 1] of TThreadCalc;
      //tablica wątków
    Edits  : array [0..JobCount - 1, 0..ThreadCount - 1] of TEdit;
      //edity do wyświetlenia wyników pośrednich
      //lub kolejności
    Timer: TTimer; //warto umieścić na wypadek niepowodzeń
    procedure ClearEdits(Sender: TObject);
    procedure RunThreads(aFGetSucc: boolean = false);
      //uruchamia wszystkie wątki
  end;

type
  TValue = record
    IntValue: int64;
    StrValue: string;
    IsInt   : boolean; //flaga gotowości wartości int64 do dalszych obliczeń
    IsStr   : boolean; //flaga gotowości wartości string do dalszych obliczeń
  end;

var
  Form1: TForm1;
  Value: array [0..JobCount - 1, 0..ThreadCount - 1] of TValue;
    //tablica wyników pośrednich
    //służy także do synchronizacji obliczeń
  SuccCtr: integer; //licznik do badania kolejności wykonywania zadań
                    //przez wszystkie wątki

implementation

{$R *.dfm}

constructor TThreadCalc.CreateWithPar(aTag: integer; aFGetSucc: boolean = false);
  //tworzy wątek
  //jeśli aFGetSucc = true zbiera informacje o kolejności
var
  iJob: integer;
begin
  Tag := aTag;
  FGetSucc := aFGetSucc;
  for iJob := 0 to JobCount - 1 do //ustawienie flag wartości
                                   //na brak gotowości do dalszych obliczeń
  begin
    Value[iJob, Tag].IsInt := false;
    Value[iJob, Tag].IsStr := false;
  end;
  Self := Create(false);
end;

procedure TThreadCalc.GetData; //pobranie danej do obliczenia silni
begin
  Value[Job, Tag].IntValue := StrToInt(Form1.Edits[Job, 0].Text);
  Value[Job, Tag].IsInt := true;
end;

procedure TThreadCalc.PutData; //wyświetlenie wyników pośrednich
                               //lub kolejności
var
  iJob: integer;
begin
  if not FGetSucc then
  begin
    for iJob := 1 to JobCount - 1 do
      if Value[iJob, Tag].IsInt then
        Form1.Edits[iJob, Tag].Text := IntToStr(Value[iJob, Tag].IntValue)
      else
      if Value[iJob, Tag].IsStr then
        Form1.Edits[iJob, Tag].Text := Value[iJob, Tag].StrValue
      else
        Form1.Edits[iJob, Tag].Text := '';
  end
  else
    for iJob := 0 to JobCount - 1 do
      Form1.Edits[iJob, Tag].Text := IntToStr(Succ[iJob]);
  if Tag = 0 then
    Form1.Edits[0, Tag].ReadOnly := false;
end;

procedure TThreadCalc.CalcProc; //procedura obliczeniowa
  procedure Job0;
  begin
    Job := 0;
    Synchronize(GetData); //używamy Synchronize po pobieramy daną z Form1
                          //tj. wątku głównego
    GetSucc;
  end;
  procedure Job1; //zadanie przygotowuje iloczyn częściowy
                  //np. dla 12! i Tag = 1 jest to 4 * 5 * 6
  var
    s: string;
    v: int64;
    i: integer;
    Max, Min: int64;
  begin
    Job := 1;
    v := Value[Job - 1, Tag].IntValue;
    Min := Tag + Tag * (v div (ThreadCount + 1)) + 1;
    Max := Tag + (Tag + 1) * (v div (ThreadCount + 1)) + 1;
    if Tag = ThreadCount - 1 then
      Max := v;
    if Max > v then
      Max := v;
    for i := Min to Max do
     if i = Min then
       s := IntToStr(i)
     else
       s := s + '*' + IntToStr(i);
    if s = '' then
     s := '1';
    Value[Job, Tag].StrValue := s;
    Value[Job, Tag].IsStr := true;
    GetSucc;
  end;
  procedure Job2; //zadanie wylicza wartość iloczynu częściowego
  var
    v: int64;
    s: string;
  begin
    Job := 2;
    s := Value[Job - 1, Tag].StrValue;
    v := 1;
    s := s + '*';
    while Pos('*', s) > 0 do
    begin
      v := v * StrToInt(Copy(s, 1, Pos('*', s) - 1));
      s := Copy(s, Pos('*', s) + 1, Length(s));
    end;
    Value[Job, Tag].IntValue := v;
    Value[Job, Tag].IsInt := true;
    GetSucc;
  end;
  procedure Job3; //zadanie mnoży iloczyny częściowe
                  //np. dla Tag = 1 jest to
                  //iloczyn_częściowy(Tag = 0) * iloczyn_częściowy(Tag = 1)
                  //w zadaniu występuje synchronizacja wątków
  begin
    Job := 3;
    if Tag = 0 then
    begin
      Value[Job, Tag].IntValue := Value[Job - 1, Tag].IntValue;
      Value[Job, Tag].IsInt := true;
    end
    else
    begin
      while not Value[Job, Tag - 1].IsInt do //synchronizacja wątków
        ;                                    //tu: oczekiwanie na zakończenie
                                             //obliczeń przez wątek (Tag - 1)
      Value[Job, Tag].IntValue :=
        Value[Job - 1, Tag].IntValue * Value[Job, Tag - 1].IntValue;
      Value[Job, Tag].IsInt := true;
    end;
    GetSucc;
    Synchronize(PutData); //używamy Synchronize bo wpisujemy dane do Form1
                          //tj. wątku głównego
    Terminate; //jest to ostatnie zadanie i kończy wątek
  end;
begin
  Job0;
  Job1;
  Job2;
  Job3;
end;

procedure TThreadCalc.GetSucc;
begin
  if not FGetSucc then
    Exit;
  Inc(SuccCtr);
  Succ[Job] := SuccCtr;
end;


procedure TThreadCalc.Execute;
begin
  FreeOnTerminate := true;
  Priority := tpNormal;
  CalcProc;
end;

procedure TForm1.Calc(Sender: TObject); //uruchamia obliczenia
begin
  try
    if StrToInt(Edits[0, 0].Text) in [0..20] then
    begin
      Edits[0, 0].ReadOnly := true;
      if (Sender as TButton).Tag = 0 then
        RunThreads
      else
      if (Sender as TButton).Tag = 1 then
        RunThreads(true);
    end;
  except
  end;
end;

procedure TForm1.RunThreads(aFGetSucc: boolean = false); //uruchamia wszystkie wątki
var
  iThread: integer;
begin
  ClearEdits(nil);
  SuccCtr := 0;
  for iThread := 0 to ThreadCount - 1 do
    Threads[iThread] := TThreadCalc.CreateWithPar(iThread, aFGetSucc);
end;

procedure TForm1.TimerTimer(Sender: TObject);
begin
  Application.ProcessMessages;
end;

procedure TForm1.ClearEdits(Sender: TObject); //czyści edity
var
  iJob, iThread: integer;
begin
  for iJob := 0 to JobCount - 1 do
    for iThread := 0 to ThreadCount - 1 do
      if not ((iJob = 0) and (iThread = 0)) then
        Edits[iJob, iThread].Text := '';
end;


procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
//należy powiązać z OnClose w ObjectInspectorze
var
  iThread: integer;
begin
  for iThread := 0 to ThreadCount - 1 do
    try
      Threads[iThread].Terminate;
    except
    end;
end;

procedure TForm1.FormCreate(Sender: TObject);
//tworzy wszystko co jest potrzebne do sprawdzenia jak to działa
//należy powiązać z OnCreate w ObjectInspectorze
const
  constWidth = 240;
  constHeight = 21;
var
  iJob, iThread: integer;
  l: TLabel;
  b: TButton;
begin
  Width      := 1000;
  Height     := 600;
  Position   := poScreenCenter;
  Font.Name  := 'Tahoma';
  Font.Size  := 8;
  Caption := 'Obliczanie silni na 4 wątkach, podaj wartość od 0 do 20';
  for iJob := 0 to JobCount - 1 do
  begin
    l := TLabel.Create(Self);
    l.Left := 10 + iJob * constWidth;
    l.Top  := 14;
    case iJob of
      0   : l.Caption := 'Zadanie ' + IntToStr(iJob) + ', wejście';
      1, 2: l.Caption := 'Zadanie ' + IntToStr(iJob);
      3   : l.Caption := 'Zadanie ' + IntToStr(iJob) + ', wyjście';
    end;

    InsertControl(l);
    for iThread := 0 to ThreadCount - 1 do
    begin
      Edits[iJob, iThread]       := TEdit.Create(Self);
      Edits[iJob, iThread].Width := constWidth;
      Edits[iJob, iThread].Left  := 10 + iJob * constWidth;
      Edits[iJob, iThread].Top   := 30 + iThread * constHeight;
      if not ((iJob = 0) and (iThread = 0)) then
        Edits[iJob, iThread].ReadOnly := true;
      InsertControl(Edits[iJob, iThread]);
    end;
  end;
  Edits[0, 0].OnChange := ClearEdits;
  Edits[0, 0].Color := clYellow;
  Edits[JobCount - 1, ThreadCount - 1].Color := clLime;
  b := TButton.Create(Self);
  b.Left := Edits[0, 0].Left;
  b.Top  := 30 + ThreadCount * constHeight;
  b.Width := constWidth;
  b.Tag := 0;
  b.Caption := 'Oblicz';
  b.OnClick := Calc;
  InsertControl(b);
  b := TButton.Create(Self);
  b.Left := Edits[1, 0].Left;
  b.Top  := 30 + ThreadCount * constHeight;
  b.Width := constWidth;
  b.Tag := 1;
  b.Caption := 'Kolejność';
  b.OnClick := Calc;
  InsertControl(b);
  Timer := TTimer.Create(Self);
  Timer.Interval := 100;
  Timer.OnTimer := TimerTimer;
  Timer.Enabled := true;
end;

end.

4 komentarzy

kod raczej nie zabija procesorów :) jeżeli temperatura ci rośnie tak że włącza się piszczek to znaczy że chłodzenie się nie wyrabia. Choćby dlatego że może radiator niedokładnie przylega do procesora, czy nawet dlatego że jest już mocno zakurzony. W normalnych warunkach temperatura nie powinna przekraczać 70 stopni (przynajmniej jak dla mnie), nieważne jakie obciążenie.

Używam Synchronize ponieważ modyfikuję zawartość editów na formie głównej, a w takiej sytuacji (akurat na przykładzie UpdateCaption) Borland zaleca takie postępowanie. Wystarczy wyklikać File/New/...ThreadObject i samo się to wpisuje do unitu. Chciałem w tych editach pokazać obliczenia cząskowe, których nikt by nie wypisywał w postaci stringa np. '1 * 2 * 3', gdyby chodziło tylko o szybkość obliczeń, a tu chodzi także o prezentację podziału na obliczenia cząstkowe.
Jeśli chodzi o Sleep(1ms) to jest to czas strasznie długi w porównaniu do większości obliczeń.
Tak czy inaczej trzeba by dobrać odpowiednią proporcję między czasem trwania zadań (Job0, Job1,...), wynikającym z ich złożoności, a czasem trwania Sleep, żeby maksymalnie wykorzystać procesor. Można jeszcze skorzystać z GetTickCount.

Trochę się boję prowadzić dalsze testy, skoro Azarien mnie ostrzega o poważnym problemie jaki mam z chłodzeniem procesora. Niby w Bios-ie mam ustawione zabezpieczenie przed przegrzaniem procesora i do tej pory zwalniał pracę, ale rzeczywiście nigdy nie było pisku. Muszę to sprawdzić.
Ten cały artykuł powstał na bazie eksperymentu. Zanim przeczytałem wasze komentarze miałem zamiar dopisać, że ten eksperyment może być niebezpieczny dla procesora - jak się okazuje dla mojego.

Jeżeli przy obciążeniu 100% zaczyna ci piszczeć, to masz poważny problem z chłodzeniem procesora.

Jaki sens ma dzielenie tego na wątki skoro i tak wszystko wykonujesz w kontekście wątku głównego przez Synchronize?
I zamiast Synchronize(ProcessMessages); można było dać Sleep(ilosc ms)