Synchronizacja wątku głównego z pulą pobocznych, w Win32 API

Synchronizacja wątku głównego z pulą pobocznych, w Win32 API
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

Uczę się wielowątkowości na potrzeby implementacji kilku ficzerów w swoim silniku i potrzebuję porady dotyczącej tego jakiego mechanizmu synchronizacji użyć, aby wątek główny mógł cyklicznie odpalać wątki poboczne. Niżej opiszę to co chcę zrobić, w maksymalnie uproszczonej formie. Interesuje mnie rozwiązanie w czystym Win32 API lub z wykorzystaniem funkcji z API SDL3.

Problem

Główny wątek robi całą robotę, natomiast co określoną liczbę milisekund ma uruchomić zadaną liczbę wątków pobocznych, w których zostaną wykonane konkretne obliczenia. Każdy uruchamiany wątek poboczny dostaje małą paczkę danych (dwie liczby) i na jej podstawie wykonuje obliczenia. Gdy wątki poboczne pracują, wątek główny czeka aż wszystkie zakończą działanie. Gdy główny wątek jest wykonywany, wątki poboczne oczekują na kolejną robotę — w tym czasie ma być możliwość zmiany liczby wątków pobocznych.

Wymagania

  1. Wątek główny zawsze zajmuje się uruchomieniem puli wątków pobocznych.
  2. Wątek główny zawsze czeka aż wszystkie poboczne zakończą obliczenia — ma wznowić działanie tylko jeśli wszystkie wątki poboczne zasygnalizowały koniec obliczeń.
  3. Czas wykonywania obliczeń w wątkach pobocznych jest nieznany i może się różnić pomiędzy nimi — jeden wątek może skończyć obliczenia np. po 1ms, inny po 10ms.
  4. Gdy wątek poboczny kończy działanie, ma wstrzymać swoje działanie i czekać na sygnał od wątku głównego — nie może być niszczony po skończeniu obliczeń, a używany wielokrotnie.
  5. Ma być możliwość zmiany liczby wątków pobocznych w runtime.
  6. Wszystkie wątki, jeśli muszą czekać, muszą być zamrożone (żadne spinlocki gotujące CPU).
  7. Zamrożenie wątku nie może bazować na czymś pokroju pętli ze Sleep(1), bo spadnie precyzja i wydajność procesu.

Zapewnienia

  1. Tylko wątek główny zajmuje się tworzeniem wątków pobocznych i tylko z nim wątki poboczne muszą synchronizować działanie (wykonywanie obliczeń, informowanie wątku głównego o zakończeniu obliczeń, oczekiwanie na sygnał od wątku głównego na rozpoczęcie kolejnych obliczeń).
  2. Obliczenia nie muszą być synchronizowane pomiędzy wątkami pobocznymi (tutaj duże ułatwienie). Wątki poboczne używają wspólnych danych tylko do odczytu, a zapisują dane w osobnych buforach.
  3. Zmiana liczby wątków pobocznych wykonywana jest zawsze z poziomu wątku głównego i zawsze gdy wszystkie wątki poboczne są zamrożone — nie ma więc potrzeby przerywania pracy wątków pobocznych lub czekania aż skończą obliczenia. To też duże ułatwienie.

Tak w dużym uproszczeniu wygląda to co chcę zrobić.


Najłatwiejsze rozwiązanie, które już teraz mogę bez problemu zaimplementować, to dynamiczne tworzenie wątków pobocznych. Wątek główny, gdy potrzebuje wykonać obliczenia, tworzy ThreadNum wątków pobocznych za pomocą CreateThread, a następnie czeka aż wszystkie skończą pracę, używając WaitForMultipleObjects. Gdy zachodzi potrzeba zmiany liczby wątków, żaden wątek poboczny nie istnieje, więc wystarczy tylko zmienić wartość zmiennej ThreadNum — banalna robota.

Problem tutaj polega na tym, że jest to rozwiązanie naiwne i bardzo nieefektywne. Dla przykładu, przy konieczności wykonania 60 takich obliczeń na sekundę rozproszonych na 8 wątkach, w każdej sekundzie działania silnik tworzyłby i niszczył 480 wątków. Trochę absurd, biorąc pod uwagę koszt tworzenia wątku i jego niszczenia.

Byłoby łatwo, gdyby wątek poboczny kończący działanie, mógł zostać wznowiony (zrestartowany, np. za pomocą ResumeThread), ale tego nie można robić — wątek przerwany (TerminateThread) lub który zakończył działanie, nie nadaje się już do niczego, trzeba go zniszczyć. Sprawdziłem to, zarówno w dokumentacji, jak i w testowym programie, aby się upewnić.


Żeby wątek poboczny mógł być raz stworzony i działać dowolnie długo (aby uniknąć jego tworzenia i niszczenia w kółko), funkcja wątku musi działać w pętli:

  • czekaj na sygnał od wątku głównego,
  • sprawdź czym jest sygnał:
    • rozkaz wykonania kolejnych obliczeń:
      • wykonaj nowe obliczenia,
      • zasygnalizuj głównemu wątkowi zakończenie obliczeń.
    • rozkaz zakończenia działania:
      • przerwanie działania funkcji wątku,
      • zasygnalizowanie wątkowi głównemu, że wątek poboczny może być zniszczony.

Być może sygnał od głównego wątku może być jeden, czyli tylko rozkaz wykonania kolejnego pakietu obliczeń, dlatego że jeśli wątek jest zamrożony, to zamiast sygnalizować mu aby zakończył działanie, wątek główny może go zniszczyć (np. za pomocą TerminateThread). Ale jeśli tak, to taki oczekujący wątek poboczny musi zostać zniszczony w taki sposób, aby jego zasoby były poprawnie finalizowane.

Co możecie zaproponować jako mechanizm synchronizacji wątku głównego z wątkiem pobocznym, aby oba mogły się porozumiewać i czekać jedno na drugie?

System zapewnia dość sporo różnych mechanizmów (muteksy, sekcje krytyczne, timery, zdarzenia, atomówki itd.), a także trochę funkcji zamrażających pracę wątków bez zżerania czasu CPU, ale jakoś nie potrafię wybrać któregokolwiek, tak aby spełniał założenia projektowe.

Prosiłbym o odpowiedzi tych, którzy rozumieją wielowątkowość i dobrze ją znają — uniknijmy przerzucania się dywagacjami i przeklejania formułek z Internetu. Linki i przykłady jak zwykle mile widziane, mogą być również z C/C++, byle chodziło o czyste Win32 API lub SDL3. 😉


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 2x, ostatnio: flowCRANE
DR
  • Rejestracja:10 miesięcy
  • Ostatnio:około 2 miesiące
  • Postów:14
1

Opiszę moją propozycję.

Jeśli wątek główny z każdym wątkiem pobocznym ma się porozumiewać niezależnie, czyli w dowolnej chwili ma mu wydać dowolny rozkaz, to wystarczy:

  1. Utworzyć listę tych wątków pobocznych - tyle ile potrzebujesz, przykładowo niech będzie 8.
  2. Na każdy wątek poboczny utworzyć dedykowany dla niego osobny obiekt sekcji krytycznej.
  3. Do synchronizacji między wątkiem głównym a pobocznym, wystarczy ta sekcja krytyczna i to właśnie proponuję.

Od strony wątku głównego, by sterować wątkiem pobocznym:

  1. Wchodzisz do sekcji krytycznej (tej przypisanej do konkretnego wątku pobocznego).
  2. Robisz co chcesz, czyli albo sprawdzasz status, czy wątek poboczny zakończył swoje zadanie i pobierasz wyniki, albo dajesz mu rozkaz z nowym zadaniem i parametrami do obliczeń.
  3. Wychodzisz z sekcji krytycznej.

Od strony wątku pobocznego taka pętla:

  1. Wchodzisz do sekcji krytycznej (tej przypisanej do konkretnego wątku pobocznego).
  2. Zależy co wątek robi:
    --> ustawiasz status o zakończeniu wykonania danego zadania, a także ustawiasz te wyniki obliczeń
    --> sprawdzasz czy jest nowy rozkaz i parametry do obliczeń
  3. Wychodzisz z sekcji krytycznej.
  4. Poza sekcją krytyczną:
    --> jeśli jest coś do wykonania, to realizujesz te obliczenia

Same wątki poboczne możesz na końcu zatrzymać i zniszczyć bezpośrednio poleceniami z poziomu wątku głównego (na podstawie tej listy wątków co zostały utworzone). Czyli dajesz rozkaz zakończenia każdego wątku pobocznego (Terminate).
W wątkach pobocznych wystarczy wtedy przerwać wykonywanie aktualnych zadań i wyjść z pętli głównej.
Wątek główny wtedy musi zaczekać, aż wszystkie wątki zostaną zakończone i wtedy może je zwolnić i usunąć ze swojej listy wątków pobocznych.

flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

Dzięki za odpowiedź. Wiesz co, próbuję dokładnie właśnie w ten sposób to rozgryźć, czyli mieć po jednej sekcji krytycznej na każdy wątek poboczny. Przy starcie programu, w wątku głównym tworzę n sekcji krytycznych i od razu do nich wchodzę. Następnie tworzę n wątków pobocznych i je uruchamiam — każdy z workerów ma jako pierwszą instrukcję oczekiwanie na sekcję krytyczną, a że wątek główny je blokuje, to wszystkie wątki poboczne domyślnie czekają.

Kiedy już inicjalizacja zostaje zakończona, wątek główny wychodzi ze wszystkich sekcji krytycznych, aby wątki poboczne zobaczyły te sekcje jako wolne i wchodzą w te sekcje krytyczne (każdy wątek do swojej). Wątek wykonuje zadanie i wychodzi z sekcji krytycznej, dzięki czemu wątek główny może do nich wejść.

Problem tylko w tym, że jeśli główny wątek wychodzi z sekcji krytycznych, aby wątki poboczne mogły wykonać swoje zadanie (albo wykonać obliczenia, albo zakończyć działanie), to muszę jakoś poczekać, aż wszystkie te sekcje zostaną odblokowane przez wątki poboczne. Tak więc robię to w pętli, próbując wejść do każdej z tych sekcji ponownie. No i to na razie nie działa — mam albo race condition i/lub dodatkowe błędy.


Tutaj jest mój program testowy, na razie tak prosty jak tylko się da:

Kopiuj
uses
  Windows;

const
  // Liczba wątków pobocznych (na razie niezmienna).
  THREAD_NUM_MAX = 4;

type
  // Pakiet danych dla każdego z wątku, dostarczany w parametrze "ThreadFunc".
  PThreadParam = ^TThreadParam;
  TThreadParam = record
    Index:  DWORD;   // Indeks wątku, na potrzeby dostępu do poniższych tablic.
    Finish: Boolean; // Flaga określająca, że wątek ma zakończyć działanie.
  end;

var
  Sections:  array [0 .. THREAD_NUM_MAX - 1] of CRITICAL_SECTION; // Sekcje krytyczne, po jednej na wątek poboczny.
  Threads:   array [0 .. THREAD_NUM_MAX - 1] of HANDLE;           // Uchwyty wszystkich wątków pobocznych.
  ThreadsID: array [0 .. THREAD_NUM_MAX - 1] of DWORD;            // ID wszystkich wątków pobocznych (nieużywane).
  Params:    array [0 .. THREAD_NUM_MAX - 1] of TThreadParam;     // Pakiety danych dla każdego wątku pobocznego.


  // Funkcja-workera, wykonywana w ramach wątku pobocznego.
  function ThreadFunc (AParameter: LPVOID): DWORD; stdcall;
  var
    // Zamiast rzutowania "AParameter" na "PThreadParam", zmień typ danych parametru.
    Param: PThreadParam absolute AParameter;
  begin
    repeat
      // Poczekaj aż wątek główny odblokuje sekcje krytyczną.
      EnterCriticalSection(@Sections[Param^.Index]);

      // Jeśli flaga jest zgaszona, wykonaj zadanie.
      if not Param^.Finish then
      begin
        // Wykonaj zadanie i zwolnij sekcję krytyczną, aby główny wątek mógł wejść do niej.
        WriteLn('Thread ', Param^.Index, ' did the job.');
        LeaveCriticalSection(@Sections[Param^.Index]);
      end
      else
      begin
        // Flaga jest zapalona, więc odblokuj sekcję krytyczną i zakończ działanie wątku.
        WriteLn('Thread ', Param^.Index, ' finished.');
        LeaveCriticalSection(@Sections[Param^.Index]);
        exit(0);
      end;
    until False;
  end;


var
  Spin:  Integer = 5; // Liczba iteracji głównej pętli.
  Index: Integer;
begin
  // Inicjalizacja zasobów.
  for Index := 0 to THREAD_NUM_MAX - 1 do
  begin
    // Inicjalizacja pakietów dla poszczególnych wątków.
    Params[Index].Index  := Index;
    Params[Index].Finish := False;

    // Stwórz po jednej sekcji dla wątku i wejdź do niej, zanim wątki zostaną stworzone.
    InitializeCriticalSection (@Sections[Index]);
    EnterCriticalSection      (@Sections[Index]);

    // Stwórz wątki poboczne. Ponieważ sekcje krytyczne są blokowane przez główny wątek, każdy
    // z wątków pobocznych powinien domyślnie czekać na ich odblokowanie.
    Threads[Index] := CreateThread(nil, 0, @ThreadFunc, @Params[Index], 0, ThreadsID[Index]);
  end;

  // Główna pętla.
  repeat
    // Zwolnij wszystkie sekcje krytyczne, aby wątki poboczne mogły zakończyć oczekiwanie i
    // wykonać swoje zadanie, na podstawie danych istniejących w tablicy parametrów.
    for Index := 0 to THREAD_NUM_MAX - 1 do
      LeaveCriticalSection(@Sections[Index]);

    // Tutaj trzeba poczekać, aż wszystkie wątki poboczne wykonają zadanie (lub zakończą działanie,
    // jeśli flaga "Finish" jest zapalona). W teorii wątek główny powinien faktycznie poczekać.
    for Index := 0 to THREAD_NUM_MAX - 1 do
      EnterCriticalSection(@Sections[Index]);

    // Zakończ działanie głównej pętli, gdy zadana liczba iteracji została wykonana.
    Spin -= 1;
  until Spin = 0;

  // Finalizacja zasobów.
  for Index := 0 to THREAD_NUM_MAX - 1 do
  begin
    // Ustaw flagę określającą, że wątki poboczne mają zostać zakończone.
    Params[Index].Finish := True;

    // Wyjdź z sekcji krytycznej, aby wątek poboczny mógł wejść do niej i zakończyć działanie.
    // Poczekaj za pomocą "WaitForSingleObject" aż wątek faktycznie zakończy działanie.
    LeaveCriticalSection(@Sections[Index]);
    WaitForSingleObject(Threads[Index], INFINITE);

    // Zwolnij uchwyt wątku oraz zniszcz dedykowaną mu sekcję krytyczną.
    CloseHandle(Threads[Index]);
    DeleteCriticalSection(@Sections[Index]);
  end;

  // Oczekiwanie z zamknięciem konsoli.
  Write('Press ENTER to exit...');
  ReadLn();
end.

Niestety po uruchomieniu tego programu, wątki poboczne nie działają wedle założeń — synchronizacja w ogóle nie działa. W konsoli od razu dostaję komunikat, że wszystkie wątki zakończyły działanie:

Kopiuj
Thread 0 finished.
Thread 1 finished.
Thread 2 finished.
Thread 3 finished.
Press ENTER to exit...

Niezbyt rozumiem dlaczego tak się dzieje. Na razie nie ma w tym testerze sprawdzania błędów, ale pod debuggerem widzę, że wszystktie sekcje krytyczne są poprawnie inicjalizowane i wszystkie wątki są poprawnie tworzone. Na razie chcę po prostu znaleźć działające rozwiązanie, a potem sobie dodam sprawdzanie błędów i inne rzeczy (np. dynamiczną zmianę liczby wątków).


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 3x, ostatnio: flowCRANE
GS
  • Rejestracja:ponad 14 lat
  • Ostatnio:3 dni
1

Nie wiem czy Free Pascal (Lazarus) ma coś podobnego do klasy Tinterlocked z Delphi.
To jest świetne narzędzie do wielowątkowej synchronizacji dostępu do wspólnych zasobów.

W tym celu deklaruję sobie zmienną (Integer) do której mają dostęp wszyskie wątki i inicjuję ją wartoscią zero.
W pewnym sensie to jest taki semafor który jeśli jest równy zero to jest otwarty i zezwala na dostęp do wspólnego zasobu,
a każda niezerowa wartość oznacza że jest zamknięty przez inny wątek.

Kopiuj
var 
  lockFlag:integer;

function lock: boolean;
begin
    result := tinterlocked.Increment(lockFlag) = 1;
    if not result then
      tinterlocked.Decrement(self.lockFlag);
end;

function unlock: boolean;
begin
  result := tinterlocked.Decrement(lockFlag) = 0;
end;

initialization 
  lockFlag:=0;
Kopiuj


begin
  if lock then
    try

    finally
      unlock;
    end;
end;

edytowany 1x, ostatnio: grzegorz_so
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

@grzegorz_so: Free Pascal ma od cholery tych funkcji Interlocked*. Nie wiem tylko jak tego typu rzeczy miałyby mi pomóc w implementacji tego co potrzebuję. Pamiętaj, że ta synchronizacja jest potrzebna tylko i wyłącznie do tego, aby główny wątek czekał na poboczne, a poboczne na główny (na zmianę), natomiast dane przetwarzane przez poboczne wątki są unikalne dla każdego z nich, więc to nie wymaga żadnej synchronizacji.

Sekcje krytyczne są cacy, dlatego że mogę dowolnie długo czekać na wejście do niej (za pomocą EnterCriticalSection) i takie oczekiwanie będzie niczym innym jak zamrożeniem wątku (a więc wątek nie będzie mielić CPU).

Co prawda mógłbym główny wątek zaprogramować tak, aby po odpaleniu wątków pobocznych po prostu spinlockiem sprawdzać czy wszystkie zakończyły działanie (widziałem taką implementację bodajże u Jonathana Blowa), ale wolałbym jednak nie robić spinlocków w ogóle.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 1x, ostatnio: flowCRANE
DR
  • Rejestracja:10 miesięcy
  • Ostatnio:około 2 miesiące
  • Postów:14
0
flowCRANE napisał(a):

Niestety po uruchomieniu tego programu, wątki poboczne nie działają wedle założeń — synchronizacja w ogóle nie działa. W konsoli od razu dostaję komunikat, że wszystkie wątki zakończyły działanie:

Kopiuj
Thread 0 finished.
Thread 1 finished.
Thread 2 finished.
Thread 3 finished.
Press ENTER to exit...

Niezbyt rozumiem dlaczego tak się dzieje. Na razie nie ma w tym testerze sprawdzania błędów, ale pod debuggerem widzę, że wszystktie sekcje krytyczne są poprawnie inicjalizowane i wszystkie wątki są poprawnie tworzone. Na razie chcę po prostu znaleźć działające rozwiązanie, a potem sobie dodam sprawdzanie błędów i inne rzeczy (np. dynamiczną zmianę liczby wątków).

W czystym Win32 API akurat nie piszę, więc nie mogę ocenić poprawności tego kodu.
Gdyby to był kod w Delphi, byłoby bardzo łatwo mi coś namierzyć.

Takie rzeczy mi się natomiast rzucają od razu:

  1. 5 powtórzeń głównej pętli to bardzo mało i może się bardzo szybko wykonać, tym bardziej jak nie ma żadnych Sleep w głównej pętli - to pewnie w ułamek sekundy się ten program zakończy.

  2. EnterCriticalSection - nie wiem po co wchodzisz do tej sekcji od każdego wątku jeszcze przed uruchomieniem głównej pętli, skoro nic i tak nie ustawiasz wtedy po wejściu do tej sekcji. W ten sposób od razu blokujesz działanie wątkom pobocznym, a skoro główna pętla się wykona pewnie w ułamek sekundy, to dlatego wątki poboczne nie zdążą nic zrobić.
    Ja bym to wywalił, skoro nic tam nie ustawiasz na starcie, a wątek poboczny i tak jeszcze nie istnieje, to jest to niepotrzebne.

Obstawiam, że te powody 1 i 2 są przyczyną, że nic się nie dzieje w wątkach pobocznych.

  1. Nie wiem czy w WIn32API masz dostęp do konstrukcji try..finally, ale tego typu instrukcje dla bezpieczeństwa powinno się pisać w ten sposób:
Kopiuj
EnterCriticalSection;
try
  // ... jakiś kod
finally
  LeaveCriticalSection;
end;

i taki schemat radził bym stosować w głównej pętli, jak i w wątkach pobocznych.

flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0
Drunky napisał(a):

Takie rzeczy mi się natomiast rzucają od razu:

  1. 5 powtórzeń głównej pętli to bardzo mało i może się bardzo szybko wykonać, tym bardziej jak nie ma żadnych Sleep w głównej pętli - to pewnie w ułamek sekundy się ten program zakończy.

Tak, w tym testowym programie założenie jest takie, że ma się wykonać tak szybko jak się da, aby mieć pewność, że cała synchronizacja działa prawidłowo (i na razie nie działa). W realnym projekcie, każdy z wątków wykona swoje obliczenia w przedziale od kilku do kilkudziesięciu milisekund. Nie może być żadnych Sleepów, dlatego że wątki poboczne mają wykonać swoje zadania tak szybko jak to możliwe i oddać sterowanie do wątku głównego, bez żadnych sztucznych opóźnień.

Pamiętaj też, że założenie jest takie, że w danym momencie albo działa główny wątek, albo wątki poboczne — nigdy jedno i drugie jednocześnie. Dlatego też kiedy wątek główny działa, wątki poboczne mają czekać. Następnie główny wątek daje sygnał, aby wątki poboczne wykonywały obliczenia — wtedy wątki poboczne robią robotę, a główny czeka aż wszystkie zrobią robotę i dadzą znać, że wątek główny może kontynuować.

  1. EnterCriticalSection - nie wiem po co wchodzisz do tej sekcji od każdego wątku jeszcze przed uruchomieniem głównej pętli, skoro nic i tak nie ustawiasz wtedy po wejściu do tej sekcji. W ten sposób od razu blokujesz działanie wątkom pobocznym […]

Dokładnie o to chodzi — używam sekcji krytycznych tylko do tego, aby wątki poboczne mogły czekać, a ponieważ tuż po stworzeniu sekcji krytycznych główny wątek od razu blokuje wszystkie sekcje, to tworzenie wątków powoduje, że wątki poboczne domyślnie czekają aż wątek główny da sygnał do roboty (tym sygnałem jest odblokowanie sekcji krytycznych).

Trzeba pamiętać, że funkcja wątku pobocznego działa w nieskończonej pętli, tak aby mogła czekać, robić robotę, czekać, robić robotę i tak w kółko, aż flaga Finish zostanie zapalona przez główny wątek. I tym co dany wątek ma robić (robotę albo czekać) steruje zawsze główny wątek. To jest jedyny sygnał, który nakazuje wątkowi pobocznemu zakończyć działanie, aby można go było bezpiecznie zwolnić z pamięci.

  1. Nie wiem czy w WIn32API masz dostęp do konstrukcji try..finally […]

Mogę stosować te bloki, dlatego że są one częścią języka (Pascala), a nie API systemowego. Tyle że one przeznaczone są do używania w połączeniu z wyjątkami, a z wyjątków nie korzystam w swoim projekcie w ogóle, bo nie są do niczego potrzebne.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 2x, ostatnio: flowCRANE
DR
  • Rejestracja:10 miesięcy
  • Ostatnio:około 2 miesiące
  • Postów:14
0
flowCRANE napisał(a):

Tak, w tym testowym programie założenie jest takie, że ma się wykonać tak szybko jak się da, aby mieć pewność, że cała synchronizacja działa prawidłowo (i na razie nie działa). W realnym projekcie, każdy z wątków wykona swoje obliczenia w przedziale od kilku do kilkudziesięciu milisekund. Nie może być żadnych Sleepów, dlatego że wątki poboczne mają wykonać swoje zadania tak szybko jak to możliwe i oddać sterowanie do wątku głównego, bez żadnych sztucznych opóźnień.

Rozumiem, że nie chcesz używać Sleep.
Jednak skoro się uczysz i dla celów testowych chcesz coś sprawdzić, taki Sleep by się przydał w głównej pętli, wtedy, gdy nie blokujesz sekcji krytycznej. Do wątków pobocznych do celów testu nie potrzeba Sleepa. Chodzi o to, aby to ruszyło i by program się nie zakończył za szybko (w ułamek sekundy) i by wątki poboczne jednak miały szansę wejść do tej sekcji krytycznej nim cały program się zakończy.
W końcu to tylko program testowy.
A jak nie chcesz używać sleepa, to zwiększ licznik iteracji na większy, albo zrób jakąś nieskończoną pętlę główną dla celów tego testu.

Pamiętaj też, że założenie jest takie, że w danym momencie albo działa główny wątek, albo wątki poboczne — nigdy jedno i drugie jednocześnie.

Wątek główny i tak musi coś robić, chociażby sterować wątkami pobocznymi, dawać im polecenia i pobierać wyniki.
Czekanie na wyniki to też jakieś zadanie dla wątku głównego.

Mogę stosować te bloki, dlatego że są one częścią języka (Pascala), a nie API systemowego. Tyle że one przeznaczone są do używania w połączeniu z wyjątkami, a z wyjątków nie korzystam w swoim projekcie w ogóle, bo nie są do niczego potrzebne.

Blok try..finally nie służy tylko do obsługi wyjątków.
On zapewnia pisanie bezpiecznego kodu, bo sekcja finally wykona się zawsze (np. zwolnienie zaalokowanego wcześniej zasobu), niezależnie od tego czy wystąpi wyjątek czy nie. Można napisać kod, który nie wygeneruje wyjątku (np. jakiś warunek lub rozkaz spowoduje wyjście z jakiejś pętli czy bloku kodu), ale chcemy, by jakiś kod się jednak wykonał.
To jest często używane np. przy tworzeniu i zwalnianiu obiektów itp.
Tak samo jest to istotne przy wchodzeniu do sekcji krytycznej, by mieć pewność, że potem na pewno z niej wyjdzie.

flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0
Drunky napisał(a):

Rozumiem, że nie chcesz używać Sleep.

Nie chcę i nie mogę — to co docelowo chcę zaimplementować, ma być częścią silnika mojej gry, dlatego aby mieć z jednej strony jak największą wydajność i jednocześnie jak najmniejsze zużycie czasu CPU, cały mechanizm przełączania wątek-główny↔wątki-poboczne musi opierać się na schedulerze. Jeśli użyję Sleep, to będę tracił cenne milisekundy i framerate spadnie.

Jednak skoro się uczysz i dla celów testowych chcesz coś sprawdzić, taki Sleep by się przydał w głównej pętli, wtedy, gdy nie blokujesz sekcji krytycznej. Do wątków pobocznych do celów testu nie potrzeba Sleepa. Chodzi o to, aby to ruszyło i by program się nie zakończył za szybko (w ułamek sekundy) i by wątki poboczne jednak miały szansę wejść do tej sekcji krytycznej nim cały program się zakończy.
W końcu to tylko program testowy.

Oczywiśćie, do testów możemy się bawić do woli. Sprawdzałem co się stanie gdy dorzucę trochę Sleepów tu i tam i program działał inaczej, wąki działały, ale główna pętla nie wykonywałą się pięć razy, a działała w nieskończoność. Czyli nadal synchronizacja była nieprawidłowa, czyli główny problem pozostał taki sam.

Być może zamiast jednej sekcji krytycznej, muszę skorzystać z dwóch per wątek poboczny. Na razie próbuję to rozgryźć, bo jakbym nie kombinował, to jedna sekcja krytyczna na wątek sprawia problemy.

Wątek główny i tak musi coś robić, chociażby sterować wątkami pobocznymi, dawać im polecenia i pobierać wyniki.
Czekanie na wyniki to też jakieś zadanie dla wątku głównego.

Dokładnie. Tyle że u mnie główny wątek jakiś taki narwany i nie chce czekać. 😉

Problemem jest poniższy kod:

Kopiuj
// Zwolnij wszystkie sekcje krytyczne, aby wątki poboczne mogły zakończyć oczekiwanie i
// wykonać swoje zadanie, na podstawie danych istniejących w tablicy parametrów.
for Index := 0 to THREAD_NUM_MAX - 1 do
  LeaveCriticalSection(@Sections[Index]);

// Tutaj trzeba poczekać, aż wszystkie wątki poboczne wykonają zadanie (lub zakończą działanie,
// jeśli flaga "Finish" jest zapalona). W teorii wątek główny powinien faktycznie poczekać.
for Index := 0 to THREAD_NUM_MAX - 1 do
  EnterCriticalSection(@Sections[Index]);

To nie działa tak jakbym chciał, LeaveCriticalSection nie powoduje automagicznie, że wątek który czeka na tę sekcję krytyczną, faktycznie zablokuje tę sekcję zanim wykona się druga pętla. Czyli moje założenia są błędne. Nie wiem jak zrobić, aby wątek główny odblokował sekcje, wątki poboczne wykryły to i je zablokowały, i aby wątek główny spróbował wejść do tych sekcji krytycznych dopiero gdy wątki poboczne je wszystkie zablokują.

Zakładałem, że jeśli wątek poboczny wywoła EnterCriticalSection na zablokowanej sekcji i na nią czeka (bo jest zablokowana), to gdy wątek główny wykona LeaveCritialSection i od razu EnterCriticalSection, to system zakolejkuje sobie najpierw żądanie z wątku pobocznego, a następnie z głównego. Ale to tak nie działa w praktyce — system nie kolejkuje żądań, dlatego mam race condition.

Blok try..finally nie służy tylko do obsługi wyjątków.

Bloki te przeznaczone są wyłącznie do obsługi wyjątków, aby zapewnić wykonanie danego kodu (np. zwolnienie zasobów), gdy instrukcje w ciele tego bloku rzucą wyjątek. I tylko tyle — nie mają żadnego innego zastosowania. Jeśli dany zestaw instrukcji nie może rzucić wyjątku, używanie tego bloku nie ma żadnego sensu.

Tak samo jest to istotne przy wchodzeniu do sekcji krytycznej, by mieć pewność, że potem na pewno z niej wyjdzie.

Wejście do sekcji krytycznej i wyjście z niej jest gwarantowane (przez system operacyjny), dlatego EnterCriticalSection i LeaveCriticalSection są procedurami. Gdyby te operacje mogły się nie powieść, byłyby to funkcje i zwracałyby kod błędu, dostępny przez GetLastError. Możesz sprawdzić przykłady używania sekcji krytycznych w dokumentacji MSDN.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 3x, ostatnio: flowCRANE
DR
  • Rejestracja:10 miesięcy
  • Ostatnio:około 2 miesiące
  • Postów:14
0
flowCRANE napisał(a):

Oczywiśćie, do testów możemy się bawić do woli. Sprawdzałem co się stanie gdy dorzucę trochę Sleepów tu i tam i program działał inaczej, wąki działały, ale główna pętla nie wykonywałą się pięć razy, a działała w nieskończoność. Czyli nadal synchronizacja była nieprawidłowa, czyli główny problem pozostał taki sam.

Być może zamiast jednej sekcji krytycznej, muszę skorzystać z dwóch per wątek poboczny. Na razie próbuję to rozgryźć, bo jakbym nie kombinował, to jedna sekcja krytyczna na wątek sprawia problemy.

Jedna sekcja krytyczna na wątek jest wystarczająca.
Jeśli coś nie działa prawidłowo, to pewnie coś jest nie tak w tym kodzie.

A nie wolałbyś najpierw sobie tego napisać w Lazarusie, a jak nabierzesz doświadczenia z wątkami, to wtedy przepisać to sobie na WinAPI?

Głowna pętla działała w "nieskończoność", a nie 5 razy, bo główna pętla oczekuje, by w jednym momencie wszystkie sekcje krytyczne będą dostępne akurat dla tej głównej pętli (a to może nigdy nie nastąpić, jak wątki poboczne ciągle wchodzą do jakiejś sekcji i nic innego nie robią poza tą sekcją krytyczną, ani nie mają chwili wytchnienia.

Nie masz pewności w jakiej kolejności i jaki wątek się dobije do danej sekcji krytycznej, zatem z poziomu pętli głównej trzeba to robić po kolei i by nie blokować działania wątkom pobocznym, tylko dać im możliwość działania tak szybko jak się da, czyli szybko odblokowywać im te sekcje krytyczne.
Zauważ, że wątki poboczne obecnie nic nie robią. Docelowo mają wykonywać jakieś zadania i to poza sekcją krytyczną - czyli w momencie gdy będą coś robić, to mają jej nie blokować.

Niepotrzebnie chcesz blokować wszystkie sekcje jednocześnie. Spróbuj to zrobić po kolei:

Kolejność w głównej pętli:

Kopiuj
EnterCriticalSection(...);
try
  // tutaj np. ustawienie rozkazu dla wątku pobocznego

  // tutaj główny wątek powinien być krótko, jak już wejdzie do tej sekcji
  
finally
  LeaveCriticalSection(...)
end;	
Sleep(..)

Dla testu możesz dać tutaj ten Sleep (wiem, że go nie chcesz).

Podobną konstrukcję możesz użyć w wątku pobocznym (dla testu też możesz dać Sleep) - chodzi o to, by wykluczyć, że cały czas wątek poboczny zajmie sekcję krytyczną, wtedy pętla główna się do niej może nie dobić i stąd wrażenie, że pętla działa w nieskończoność.

W wątku pobocznym dodaj jakieś zadanie, co się ma wykonać wtedy, gdy nie jest w sekcji krytycznej (aby jej nie zajmować niepotrzebnie).

I nie blokuj od razu wszystkich wątków pobocznych i bez odblokowywania tych sekcji (czyli trzymasz wszystkie zablokowane),
tylko po jednej sekcji: wchodzisz, coś robisz i szybko wychodzisz z sekcji.
I to samo dla następnego wątku pobocznego.

Zakładałem, że jeśli wątek poboczny wywoła EnterCriticalSection na zablokowanej sekcji i na nią czeka (bo jest zablokowana), to gdy wątek główny wykona LeaveCritialSection i od razu EnterCriticalSection, to system zakolejkuje sobie najpierw żądanie z wątku pobocznego, a następnie z głównego. Ale to tak nie działa w praktyce — system nie kolejkuje żądań, dlatego mam race condition.

Nie ma takiego kolejkowania i gwarancji, który wątek się pierwszy dobije do sekcji krytycznej.

Bloki te przeznaczone są wyłącznie do obsługi wyjątków, aby zapewnić wykonanie danego kodu (np. zwolnienie zasobów), gdy instrukcje w ciele tego bloku rzucą wyjątek. I tylko tyle — nie mają
żadnego innego zastosowania. Jeśli dany zestaw instrukcji nie może rzucić wyjątku, używanie tego bloku nie ma żadnego sensu.

Nie jest to prawdą, że blok try..finally służy tylko do obsługi wyjątków. To tylko jedno z jego zastosowań, ale nie jedyne.
Opisałem już do czego służy. Poniżej prosty przykład:

Kopiuj
EnterCriticalSection;
try
  for I := 1 to 100 do
  begin
    // jakiś warunek zakończenia pętli
    if I = 50 then Exit;
  end;
finally
  // ten kod wykona się zawsze, nawet jak nie wystąpi żaden wyjątek
  // w tym wypadku, gdy I = 50
  // zapewnienie zwolnienia sekcji krytycznej, nawet jak nie ma żadnego wyjątku
  // gdyby poniższa instrukcja nie była w bloku finally, to nigdy by się nie wykonała dla tego przykładowego kodu,
  // a tak, to wykona się zawsze
  LeaveCriticalSection;
  // (albo zwolnienie jakiegoś zasobu itp.)
end;

Wejście do sekcji krytycznej i wyjście z niej jest gwarantowane (przez system operacyjny), dlatego EnterCriticalSection i LeaveCriticalSection są procedurami. Gdyby te operacje mogły się nie powieść, byłyby to funkcje i zwracałyby kod błędu, dostępny przez GetLastError. Możesz sprawdzić przykłady używania sekcji krytycznych w dokumentacji MSDN.

Wejście czy wyjście z sekcji krytycznej to są owszem procedury, jednak wykonają się dopiero wtedy gry program faktycznie skoczy do tych instrukcji.
Jeśli sam program będzie błędnie napisany (np błąd logiczny, błędne wartości jakich nie przewidziałeś itp.), wtedy może nie dojść do ich wykonania.
Blok try..finally (jak w powyższym przykładzie) zapewnia, że ten fragment kodu się wykona, niezależnie od tego jakie intrukcje będą wcześniej (przecież może być błąd logiczny, błąd w kodzie itp. co nie spowoduje wyjątku, tylko wyjście z jakiegoś bloku).

edytowany 1x, ostatnio: Drunky
KA
  • Rejestracja:prawie 20 lat
  • Ostatnio:mniej niż minuta
  • Lokalizacja:Gorlice
2

Wersja Delphi (jakby ktoś chciał potestować):

Kopiuj
program ThreadsTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  WinApi.Windows;

const
  // Liczba wątków pobocznych (na razie niezmienna).
  THREAD_NUM_MAX = 4;

type
  // Pakiet danych dla każdego z wątku, dostarczany w parametrze "ThreadFunc".
  PThreadParam = ^TThreadParam;
  TThreadParam = record
    Index:  DWORD;   // Indeks wątku, na potrzeby dostępu do poniższych tablic.
    Finish: Boolean; // Flaga określająca, że wątek ma zakończyć działanie.
  end;

var
  Sections:  array [0 .. THREAD_NUM_MAX - 1] of _RTL_CRITICAL_SECTION; // Sekcje krytyczne, po jednej na wątek poboczny.
  Threads:   array [0 .. THREAD_NUM_MAX - 1] of THANDLE;           // Uchwyty wszystkich wątków pobocznych.
  ThreadsID: array [0 .. THREAD_NUM_MAX - 1] of DWORD;            // ID wszystkich wątków pobocznych (nieużywane).
  Params:    array [0 .. THREAD_NUM_MAX - 1] of TThreadParam;     // Pakiety danych dla każdego wątku pobocznego.


  // Funkcja-workera, wykonywana w ramach wątku pobocznego.
  function ThreadFunc (AParameter: LPVOID): DWORD; stdcall;
  var
    // Zamiast rzutowania "AParameter" na "PThreadParam", zmień typ danych parametru.
    Param: TThreadParam;
  begin
    
    repeat
      Param:= TThreadParam(AParameter^);
      
      // Poczekaj aż wątek główny odblokuje sekcje krytyczną.
      EnterCriticalSection(Sections[Param.Index]);

      // Jeśli flaga jest zgaszona, wykonaj zadanie.
      if not Param.Finish then
      begin
        // Wykonaj zadanie i zwolnij sekcję krytyczną, aby główny wątek mógł wejść do niej.
        WriteLn('Thread ', Param.Index, ' did the job.');
        LeaveCriticalSection(Sections[Param.Index]);
      end
      else
      begin
        // Flaga jest zgaszona, więc odblokuj sekcję krytyczną i zakończ działanie wątku.
        WriteLn('Thread ', Param.Index, ' finished.');
        LeaveCriticalSection(Sections[Param.Index]);
        exit(0);
      end;
    until False;
  end;


var
  Spin:  Integer = 5; // Liczba iteracji głównej pętli.
  Index: Integer;
begin
  // Inicjalizacja zasobów.
  for Index := 0 to THREAD_NUM_MAX - 1 do
  begin
    // Inicjalizacja pakietów dla poszczególnych wątków.
    Params[Index].Index  := Index;
    Params[Index].Finish := False;

    // Stwórz po jednej sekcji dla wątku i wejdź do niej, zanim wątki zostaną stworzone.
    InitializeCriticalSection (Sections[Index]);
    EnterCriticalSection      (Sections[Index]);

    // Stwórz wątki poboczne. Ponieważ sekcje krytyczne są blokowane przez główny wątek, każdy
    // z wątków pobocznych powinien domyślnie czekać na ich odblokowanie.
    Threads[Index] := CreateThread(nil, 0, @ThreadFunc, @Params[Index], 0, ThreadsID[Index]);
  end;

  // Główna pętla.
  repeat
    // Zwolnij wszystkie sekcje krytyczne, aby wątki poboczne mogły zakończyć oczekiwanie i
    // wykonać swoje zadanie, na podstawie danych istniejących w tablicy parametrów.
    for Index := 0 to THREAD_NUM_MAX - 1 do
      LeaveCriticalSection(Sections[Index]);

    // Tutaj trzeba poczekać, aż wszystkie wątki poboczne wykonają zadanie (lub zakończą działanie,
    // jeśli flaga "Finish" jest zapalona). W teorii wątek główny powinien faktycznie poczekać.
    for Index := 0 to THREAD_NUM_MAX - 1 do
      EnterCriticalSection(Sections[Index]);

    // Zakończ działanie głównej pętli, gdy zadana liczba iteracji została wykonana.
    Spin:= Spin - 1;
  until Spin = 0;

  // Finalizacja zasobów.
  for Index := 0 to THREAD_NUM_MAX - 1 do
  begin
    // Ustaw flagę określającą, że wątki poboczne mają zostać zakończone.
    Params[Index].Finish := True;

    // Wyjdź z sekcji krytycznej, aby wątek poboczny mógł wejść do niej i zakończyć działanie.
    // Poczekaj za pomocą "WaitForSingleObject" aż wątek faktycznie zakończy działanie.
    LeaveCriticalSection(Sections[Index]);
    WaitForSingleObject(Threads[Index], INFINITE);

    // Zwolnij uchwyt wątku oraz zniszcz dedykowaną mu sekcję krytyczną.
    CloseHandle(Threads[Index]);
    DeleteCriticalSection(Sections[Index]);
  end;

  // Oczekiwanie z zamknięciem konsoli.
  Write('Press ENTER to exit...');
  Readln;
end.

Moje zmiany to przede wszystkim pobieranie parametru w ThreadFunc a dalej nie bawię się ze wskaźnikami...
No i w parametrach funkcji nie mam @ bo to chyba tylko wymóg dialektu FP.

Wyniki u mnie:

Kopiuj
Thread 0 did the job.
Thread 0 finished.
Thread 1 finished.
Thread 2 did the job.
Thread 2 finished.
Thread 3 did the job.
Thread 3 finished.
Press ENTER to exit...

Thread 0 finished.
Thread 1 did the job.
Thread 1 finished.
Thread 2 did the job.
Thread 2 finished.
Thread 3 did the job.
Thread 3 finished.
Press ENTER to exit...

Thread 0 finished.
Thread 1 finished.
Thread 2 did the job.
Thread 2 finished.
Thread 3 did the job.
Thread 3 finished.
Press ENTER to exit...

i inne śmieszne cuda a bez zmian było dokładnie jak u Ciebie.Wygląda na to, że albo niektóre wątki w ogóle nie zdążą wykonać roboty albo dalej coś popieprzone... Nie wiem nie mam siły nad tym siedzieć.
A może coś takiego z Eventami by się nadało https://learn.microsoft.com/en-us/windows/win32/sync/using-event-objects ?


Nie odpowiadam na PW w sprawie pomocy programistycznej.
Pytania zadawaj na forum, bo:
od tego ono jest ;) | celowo nie zawracasz gitary | przeczyta to więcej osób a więc większe szanse że ktoś pomoże.
edytowany 1x, ostatnio: kAzek
flowCRANE
U Ciebie są większe cuda niż u mnie, ale przynajmniej wątki poboczne coś tam robią. :D
flowCRANE
Czytałem o tych eventach i nie widziałem dla nich zastosowania. Ale przeczytam jeszcze raz.
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0
Drunky napisał(a):

Jedna sekcja krytyczna na wątek jest wystarczająca.

Kurczę, też tak myślałem, ale coś jest nie tak z moim kodem i nie wiem w sumie czy to faktycznie jest możliwe, używając jednej sekcji per wątek.

A nie wolałbyś najpierw sobie tego napisać w Lazarusie, a jak nabierzesz doświadczenia z wątkami, to wtedy przepisać to sobie na WinAPI?

Nie bardzo wiem jak by to miało pomóc — w końcu jeśli użyję klasy TThread, to będę miał to samo, tyle że opakowane w klasę.

Zauważ, że wątki poboczne obecnie nic nie robią. Docelowo mają wykonywać jakieś zadania i to poza sekcją krytyczną - czyli w momencie gdy będą coś robić, to mają jej nie blokować.

Tylko że jeśli nie będą niczego blokować, to ani główny wątek nie będzie wiedział kiedy skończą obliczenia, ani też nie będą w stanie czekać na kolejny rozkaz wykonywania obliczeń. Dlatego też wątek poboczny blokuje sekcję krytyczną, aby główny wątek wiedział że ma czekać. I to na razie nie działa tak jakbym chciał — albo mam gdzieś bug, albo to problem X/Y. 😉

Niepotrzebnie chcesz blokować wszystkie sekcje jednocześnie. Spróbuj to zrobić po kolei:

Poczekaj, bo Twoja propozycja nie jest do końca dobra. Ogólnie chodzi o to, aby gdy wątek główny działa, wątki poboczne mają być zamrożone. Dlatego w trakcie działania głównego wątku, wszystkie sekcje krytyczne są zablokowane i stąd wątki poboczne wiedzą, że mają czekać (czekają na ich odblokowanie i wtedy robią robotę).

Gdy wątek główny odblokowuje sekcje krytyczne, wtedy wątki poboczne kończą oczekiwanie i albo robią obliczenia, albo kończą działanie. Po odblokowaniu sekcji krytycznych i starcie wątków pobocznych, wątek główny musi poczekać aż poboczne skończą obliczenia. Wątki poboczne zwalniają sekcje krytyczne i w ten sposób główny wątek informowany jest, że ma kontynuować.


Uprośćmy ten problem do jednego wątku pobocznego, żeby mieć łatwiejsze zadanie. Czyli wątek główny ma stworzyć jeden wątek poboczny i program ma działać tak, że wątki działają na przemian — główny, poboczny, główny, poboczny itd., nigdy oba naraz, jedno czeka na drugie. Jedno musi czekać na drugie. Po określonej liczbie iteracji pętli głównej (dla testu to pięć iteracji), główna pętla kończy działanie i niszczy wątek poboczny.

Jeśli macie np. obiektowy przykład, z użyciem klasy TThread, który by realizował te proste założenia, to z chęcią się z nim zapoznam i spróbuję przetłumaczyć na niskopoziomowy. Sam się tym jeszcze pobawię, spróbuję rozgryźć temat używając dwóch sekcji krytycznych per wątek poboczny.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 1x, ostatnio: flowCRANE
DR
  • Rejestracja:10 miesięcy
  • Ostatnio:około 2 miesiące
  • Postów:14
3
flowCRANE napisał(a):

Nie bardzo wiem jak by to miało pomóc — w końcu jeśli użyję klasy TThread, to będę miał to samo, tyle że opakowane w klasę.

Wtedy ja bym łatwiej mógł sprawdzić taki kod w Delphi i bym łatwiej mógł pomóc.

Tylko że jeśli nie będą niczego blokować, to ani główny wątek nie będzie wiedział kiedy skończą obliczenia, ani też nie będą w stanie czekać na kolejny rozkaz wykonywania obliczeń. Dlatego też wątek poboczny blokuje sekcję krytyczną, aby główny wątek wiedział że ma czekać. I to na razie nie działa tak jakbym chciał — albo mam gdzieś bug, albo to problem X/Y. 😉

Niepotrzebnie chcesz blokować wszystkie sekcje jednocześnie. Spróbuj to zrobić po kolei:

Poczekaj, bo Twoja propozycja nie jest do końca dobra. Ogólnie chodzi o to, aby gdy wątek główny działa, wątki poboczne mają być zamrożone. Dlatego w trakcie działania głównego wątku, wszystkie sekcje krytyczne są zablokowane i stąd wątki poboczne wiedzą, że mają czekać (czekają na ich odblokowanie i wtedy robią robotę).

Teraz już widzę o co Tobie chodzi i skąd wynika między nami nieporozumienie odnośnie tego kodu i działania tych sekcji krytycznych.
Ty napisałeś swój kod z myślą o blokowaniu i zamrażaniu wątków.

Ja opisałem moją propozycję w zupełnie innym celu. Sekcje krytyczne zaproponowałem tylko do synchronizacji wymiany informacji między wątkami i zabezpieczenia dostępu do współdzielonych danych, by różne wątki nie mogły jednocześnie odczytać/zmodyfikować tych samych zmiennych.
Moja propozycja nie miała zamrażać na długo wątków, tylko umożliwiać łatwą wymianę informacji między nimi.
Zarówno wątek główny mógłby szybko sprawdzić stan każdego wątku pobocznego, jak i wątek poboczny mógłby sprawdzić, czy ma coś do wykonania zlecone od wątku głównego.
I wątek główny by wiedział, kiedy dany wątek poboczny skończy obliczenia, przecież ma dostęp do zmiennej Finish: Boolean od każdego wątku - wystarczyłoby dopisać, by wątek poboczny ją ustawił po wykonaniu swojego zadania na True.
Wątek główny miał w zasadzie niewiele robić, tylko co chwilę (i na krótką chwilę) wchodzić do tych sekcji krytycznych, sprawdzać co robi dany wątek poboczny (czy skończył zadanie) i gromadzić wyniki lub dawać mu nowe rozkazy (jakie obliczenia ma wykonać).

Natomiast używanie sekcji krytycznych do blokowania całych wątków, a także wątku głównego, to nie jest dobry pomysł. Tego nie proponuję.
Nie ma pewności który wątek pierwszy się dobije i wejdzie do sekcji krytycznej, ani kiedy to nastąpi.
Do tego jak po kolei chcesz wchodzić do każdej sekcji krytycznej w wątku głównym (na starcie dla wątku pobocznego numer 1), to nie wiesz i tak kiedy te inne wątki zakończą działanie. Może się okazać, że wątek numer 1 zakończy działanie jako ostatni, a oczekujesz na wejście do jego sekcji krytycznej, a dopiero potem dla wątku numer 2, 3...8 itp. To byłoby bardzo niewydajne w przypadku blokad na dłużej, a zwłaszcza od blokad przez wątki poboczne.

Jeśli macie np. obiektowy przykład, z użyciem klasy TThread, który by realizował te proste założenia, to z chęcią się z nim zapoznam i spróbuję przetłumaczyć na niskopoziomowy. Sam się tym jeszcze pobawię, spróbuję rozgryźć temat używając dwóch sekcji krytycznych per wątek poboczny.

Proponuję zatem inne podejście i użycie obiektów typu TEvent, a sekcji krytycznych tylko do zabezpieczenia dostępu do współdzielonych danych.
Zrobiłem przykład w Delphi, trochę nad tym pokombinowałem i działa.
Dla testu: 8 wątków pobocznych, 2 powtórzenia głównej pętli.
Dodałem też zabezpieczenie sekcją krytyczną, by w trybie tekstowym napisy się nie nadpisywały jednocześnie z różnych wątków.
Możesz to sobie jakoś spróbować przerobić na WinAPI32.

Pełny kod:

Kopiuj
program watki_event_przyklad;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Classes,
  System.SyncObjs,
  System.Generics.Collections;

type
  TWorkerThread = class(TThread)
  private
    FThreadID: Integer;
    FStartEvent: TEvent;
  protected
    procedure Execute; override;
  public
    constructor Create(const ThreadID: Integer; const StartEvent: TEvent);
  end;

const
  THREAD_COUNT = 8;

var
  Threads: TList<TWorkerThread>;
  StartEvents: array [1..THREAD_COUNT] of TEvent;
  FinishEvent: TEvent;
  ActiveThreads: Integer;
  ActiveThreadsLock, ConsoleLock: TCriticalSection;
  lvI, lvJ: Integer;

procedure WriteToConsole(const Text: string);
begin
  ConsoleLock.Enter;
  try
    Writeln(Text);
  finally
    ConsoleLock.Leave;
  end;
end;

{ TWorkerThread }

constructor TWorkerThread.Create(const ThreadID: Integer; const StartEvent: TEvent);
begin
  FreeOnTerminate := False;
  FThreadID := ThreadID;
  FStartEvent := StartEvent;
  inherited Create(False);
end;

procedure TWorkerThread.Execute;
begin
  WriteToConsole(Format('Wątek %d uruchomiony i oczekuje na sygnał...', [FThreadID]));

  while not Terminated do
  begin
    // Czekanie na sygnał od wątku głównego
    if FStartEvent.WaitFor(INFINITE) = wrSignaled then
    begin
      FStartEvent.ResetEvent;

      if Terminated then Exit; // Sprawdzenie, czy wątek powinien się zakończyć

      // Po otrzymaniu sygnału wykonuje zadanie
      WriteToConsole(Format('Wątek %d rozpoczął pracę...', [FThreadID]));
      Sleep(10);
      WriteToConsole(Format('Wątek %d zakończył pracę.', [FThreadID]));

      // Zmniejszenie licznika aktywnych wątków
      ActiveThreadsLock.Enter;
      try
        Dec(ActiveThreads);
        if ActiveThreads = 0 then
          FinishEvent.SetEvent; // Ostatni wątek sygnalizuje zakończenie pracy dla głównego wątku
      finally
        ActiveThreadsLock.Leave;
      end;
    end;
  end;
end;

begin
  for lvI := 1 to THREAD_COUNT do
    StartEvents[lvI] := TEvent.Create(nil, True, False, 'StartEvent_' + lvI.ToString);
  FinishEvent := TEvent.Create(nil, True, False, 'FinishEvent');
  ActiveThreadsLock := TCriticalSection.Create;
  ConsoleLock := TCriticalSection.Create;
  Threads := TList<TWorkerThread>.Create;
  try
    WriteToConsole('-------------------------------------');
    WriteToConsole('START PROGRAMU');
    WriteToConsole('-------------------------------------');
    WriteToConsole('Tworzenie wątków...');

    for lvI := 1 to THREAD_COUNT do
      Threads.Add(TWorkerThread.Create(lvI, StartEvents[lvI]));

    for lvI := 1 to 2 do
    begin
      WriteToConsole('-------------------------------------');
      WriteToConsole(Format('Przebieg numer %d głównej pętli (głównego wątku)...', [lvI]));

      ActiveThreadsLock.Enter;
      try
        ActiveThreads := THREAD_COUNT;
      finally
        ActiveThreadsLock.Leave;
      end;

      WriteToConsole('Wątek główny wysyła kolejny sygnał dla wszystkich wątków pobocznych...');

      // Kolejne uruchomienie zadania (dla każdego wątku osobno)
      for lvJ := 1 to THREAD_COUNT do
        StartEvents[lvJ].SetEvent;

      // Czekanie aż wszystkie wątki zakończą swoje zadanie
      FinishEvent.WaitFor(INFINITE);

      WriteToConsole('Wszystkie wątki poboczne zakończyły wykonanie zadania.');

      FinishEvent.ResetEvent;
    end;

    WriteToConsole('-------------------------------------');
    WriteToConsole('KONIEC PROGRAMU');

  finally
    for lvI := 0 to Threads.Count - 1 do
    begin
      Threads[lvI].Terminate; // Zakończenie działania wątków
      StartEvents[lvI+1].SetEvent;
      Threads[lvI].WaitFor;
      Threads[lvI].Free;
    end;
    Threads.Free;
    for lvI := 1 to THREAD_COUNT do
      StartEvents[lvI].Free;
    FinishEvent.Free;

    ActiveThreadsLock.Free;
    ConsoleLock.Free;
  end;

  ReadLn;
end.

Tak to zalogowało u mnie:

Kopiuj
-------------------------------------
START PROGRAMU
-------------------------------------
Tworzenie wątków...
Wątek 1 uruchomiony i oczekuje na sygnał...
Wątek 2 uruchomiony i oczekuje na sygnał...
Wątek 3 uruchomiony i oczekuje na sygnał...
-------------------------------------
Przebieg numer 1 głównej pętli (głównego wątku)...
Wątek główny wysyła kolejny sygnał dla wszystkich wątków pobocznych...
Wątek 1 rozpoczął pracę...
Wątek 4 uruchomiony i oczekuje na sygnał...
Wątek 4 rozpoczął pracę...
Wątek 3 rozpoczął pracę...
Wątek 2 rozpoczął pracę...
Wątek 7 uruchomiony i oczekuje na sygnał...
Wątek 5 uruchomiony i oczekuje na sygnał...
Wątek 8 uruchomiony i oczekuje na sygnał...
Wątek 8 rozpoczął pracę...
Wątek 7 rozpoczął pracę...
Wątek 5 rozpoczął pracę...
Wątek 6 uruchomiony i oczekuje na sygnał...
Wątek 6 rozpoczął pracę...
Wątek 2 zakończył pracę.
Wątek 3 zakończył pracę.
Wątek 1 zakończył pracę.
Wątek 8 zakończył pracę.
Wątek 6 zakończył pracę.
Wątek 7 zakończył pracę.
Wątek 5 zakończył pracę.
Wątek 4 zakończył pracę.
Wszystkie wątki poboczne zakończyły wykonanie zadania.
-------------------------------------
Przebieg numer 2 głównej pętli (głównego wątku)...
Wątek główny wysyła kolejny sygnał dla wszystkich wątków pobocznych...
Wątek 1 rozpoczął pracę...
Wątek 3 rozpoczął pracę...
Wątek 4 rozpoczął pracę...
Wątek 2 rozpoczął pracę...
Wątek 5 rozpoczął pracę...
Wątek 8 rozpoczął pracę...
Wątek 6 rozpoczął pracę...
Wątek 7 rozpoczął pracę...
Wątek 7 zakończył pracę.
Wątek 5 zakończył pracę.
Wątek 6 zakończył pracę.
Wątek 3 zakończył pracę.
Wątek 2 zakończył pracę.
Wątek 4 zakończył pracę.
Wątek 1 zakończył pracę.
Wątek 8 zakończył pracę.
Wszystkie wątki poboczne zakończyły wykonanie zadania.
-------------------------------------
KONIEC PROGRAMU
edytowany 1x, ostatnio: Drunky
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

Ten programik działa we Free Pascalu (w trybie {$MODE DELPHI}, bez żadnych modyfikacji), ale zawiesza się w trakcie zwalniania wątków, w sekcji finalize, w linijce Threads[lvI].Free; — po prostu wisi w nieskończoność. U Ciebie to działa i wątki są niszczone?


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
DR
  • Rejestracja:10 miesięcy
  • Ostatnio:około 2 miesiące
  • Postów:14
1

Podmieniłem jeszcze ten fragment kodu w ostatniej części programu (zedytowałem to też w poprzednim poście).
To trzeba podmienić i będzie ok.

Kopiuj
    for lvI := 0 to Threads.Count - 1 do
    begin
      Threads[lvI].Terminate; // Zakończenie działania wątków
      StartEvents[lvI+1].SetEvent;
      Threads[lvI].WaitFor;
      Threads[lvI].Free;
    end;
flowCRANE
Racja, WaitFor brakowało — teraz działa bezbłędnie.
DR
StartEvents[lvI+1].SetEvent; <-- to też jest dosyć istotne, bo jak wątek czekał w nieskończoność na sygnał, to mógłby wisieć tak bez końca w Execute.
flowCRANE
Dokładnie, sprawdziłem i bez ustawienia zdarzenia też będzie wisieć, więc to istotne.
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

Właśnie studiowałem na MSDN to czym są zdarzenia i jaki ich używać, a tu proszę — przykład się pojawił, dzięki wielkie!

Pobawiłem się nim trochę, zmieniłem liczbę iteracji i wątków oraz przy okazji usunąłem funkcję WriteToConsole, dlatego że WriteLn może być używany bez synchronizacji we Free Pascalu (to jest zagwarantowane). No i zmieniłem komunikaty na angielskie, bo mi konsola krzaki wyświetlała (nie chce mi się bawić ze zmianą kodowania).

Przykładowe wyjście, dla czterech wątków pięciu iteracji:

Kopiuj
Creating threads...
-------------------------------------
Loop iteration 1
Thread 3 finished its job.
Thread 4 finished its job.
Thread 1 finished its job.
Thread 2 finished its job.
End of the loop iteration.
-------------------------------------
Loop iteration 2
Thread 2 finished its job.
Thread 4 finished its job.
Thread 3 finished its job.
Thread 1 finished its job.
End of the loop iteration.
-------------------------------------
Loop iteration 3
Thread 1 finished its job.
Thread 2 finished its job.
Thread 4 finished its job.
Thread 3 finished its job.
End of the loop iteration.
-------------------------------------
Loop iteration 4
Thread 1 finished its job.
Thread 2 finished its job.
Thread 3 finished its job.
Thread 4 finished its job.
End of the loop iteration.
-------------------------------------
Loop iteration 5
Thread 2 finished its job.
Thread 3 finished its job.
Thread 4 finished its job.
Thread 1 finished its job.
End of the loop iteration.

Raczej nie będzie problemu z implementacją tego w czystym Win32 API (da się to zrobić praktycznie 1:1), ale jeszcze muszę wymyślić jak zapewnić możliwość dynamicznej zmiany liczby wątków. W teorii to będzie proste, bo w końcu tak samo będzie się dodawało kolejne jak tworzyło te na początku, a zmniejszanie ich liczby to taki sam kod jak ich usuwania.

Dziś już zmęczony jestem, ale będę musiał dokładniej przyjrzeć się temu programowi, tak aby wszystko dokładnie zrozumieć. Szykuje się fajna dłubanina. 😉


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 2x, ostatnio: flowCRANE
SL
  • Rejestracja:około 7 lat
  • Ostatnio:około 5 godzin
  • Postów:890
0

Dobrą abstrakcją na przesyłanie zadań pomiędzy wątkami jak i budzeniem/spaniem jest blocking queue. Wynika to z tego, że zarówno budzenie/spanie jak i przesłanie informacji dzieje się przez ten sam prosty interfejs robiący obie operacje jednocześnie, więć ciężko jest to zepsuć. Znalazłem coś takiego TThreadedQueue, ale to chyba Delphi. Radziłbym napisać coś podobnego albo znaleźć jakąś bibliotekę

W tej konfiguracji wątki są uruchamiane na czas trwania programu. Kod wątku w pętli próbuje wyciągnąć elementy z kolejki, wątek główny możesz zakomunikować o zakończeniu obliczeń inną kolejką. Jedne czego brakuje to jakaś operacja close wysłana przez wątek główny, która przesłana do wątku przez kolejkę zakończy jego pracę

Bez tego musisz użyć condition variable bo tylko ten niskopoziomowy mechanizm pozwala na spanie/wzbudzanie (z tego co wiem). Niestety używanie czegoś takiego to proszenie się o kłopoty, lepiej napisać taką kolejkę i mieć upchany ten bajzel w ładnej abstrakcji

Z innej beczki to raczej odradzałbym używać WinApi, bo po co? Z tego co widzę to pascal ma jakieś wsparcie dla TThread. SDL też ma https://wiki.libsdl.org/SDL2/SDL_CreateCond. Z mutexami i condition variable jesteś w stanie napisać coś takiego https://chatgpt.com/share/67b5407f-bca8-8008-8e32-b9ee92e9a5c7

edytowany 1x, ostatnio: slsy
DR
  • Rejestracja:10 miesięcy
  • Ostatnio:około 2 miesiące
  • Postów:14
1
flowCRANE napisał(a):

Raczej nie będzie problemu z implementacją tego w czystym Win32 API (da się to zrobić praktycznie 1:1), ale jeszcze muszę wymyślić jak zapewnić możliwość dynamicznej zmiany liczby wątków. W teorii to będzie proste, bo w końcu tak samo będzie się dodawało kolejne jak tworzyło te na początku, a zmniejszanie ich liczby to taki sam kod jak ich usuwania.

Tak, dynamiczna zmiana liczby wątków, to już prosta sprawa.
Zamiast stałej THREAD_COUNT możesz mieć zmienną zawierającą ich ilość, no i jakąś listę/tablicę tych wątków i do nich dodawać/usuwać nowe wątki.
Najbezpieczniej będzie dodawać/usuwać nowy wątek w momencie, gdy wątki poboczne aktualnie nic nie robią (czyli gdy wszystkie zakończyły działanie).
Istotne, aby zmienna ActiveThreads:

Kopiuj
ActiveThreads := THREAD_COUNT; // tutaj zamiast stałej THREAD_COUNT podmienić na rzeczywistą liczbę wątków

była zawsze ustawiana na aktualną liczbę wątków w momencie polecenia od głównego wątku wykonywania nowych zadań przez wątki poboczne,
aby mogła potem zejść do zera po zakończeniu pracy przez ostatni wątek poboczny.

Dziś już zmęczony jestem, ale będę musiał dokładniej przyjrzeć się temu programowi, tak aby wszystko dokładnie zrozumieć. Szykuje się fajna dłubanina. 😉

Powodzenia! 🙂

edytowany 1x, ostatnio: Drunky
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0
slsy napisał(a):

Dobrą abstrakcją na przesyłanie zadań pomiędzy wątkami jak i budzeniem/spaniem jest blocking queue. Wynika to z tego, że zarówno budzenie/spanie jak i przesłanie informacji dzieje się przez ten sam prosty interfejs robiący obie operacje jednocześnie, więć ciężko jest to zepsuć.

Nie za bardzo pasuje to do mojego problemu. Ja potrzebuję prostego flop-flopu — wątek główny przełącza przełącznik i wątki poboczne robią robotę, a potem przełączają przełącznik i wątek główny kontynuuje. Działa albo jedno, albo drugie — nigdy obie rzeczy jednocześnie.

Znalazłem coś takiego TThreadedQueue, ale to chyba Delphi. Radziłbym napisać coś podobnego albo znaleźć jakąś bibliotekę

Nie używam zewnętrznych bibliotek i też nie za bardzo jest powód aby ich używać. Raz, że uczę się wielowątkowości i chcę dokładnie wiedzieć jak to wszystko działa, do samego spodu; a dwa, że biblioteki tak czy siak opakowują systemowe API, a skoro tak, to po co ich używać, skoro to samo mogę napisać sam, unikając zbędnych zależności.

To co podałeś wygląda ciekawie, ale jest to zbyt skomplikowane w porównaniu do problemu do rozwiązania — trochę armata na muchę. Implementacja od ChatGPT, mimo że całkiem solidnie wygląda, zawiera dwa razy więcej linijek kodu niż cały program testowy. Zobacz jak niewiele kodu potrzeba, aby worker czekał na sygnał, robił zadanie, a następnie czekał na kolejny sygnał:

Kopiuj
procedure TWorkerThread.Execute();
begin
  while not Terminated do
    if FStartEvent.WaitFor(INFINITE) = wrSignaled then
    begin
      if Terminated then exit;

      // tu obliczenia to zrobienia

      ActiveThreadsLock.Enter();
      ActiveThreads -= 1;

      if ActiveThreads = 0 then
        FinishEvent.SetEvent();

      ActiveThreadsLock.Leave();
    end;
end;

i jak niewiele go trzeba, aby główny wątek odpalał te poboczne i czekał aż zakończą działanie:

Kopiuj
InterlockedExchange(ActiveThreads, THREAD_COUNT);

// odpal wątki poboczne
for IndexEvent := 0 to THREAD_COUNT - 1 do
  StartEvents[IndexEvent].SetEvent();

// czekaj aż zakończą obliczenia
FinishEvent.WaitFor(INFINITE);

Plus kilka linijek deklaracji zmiennych. Trochę uprościłem tę demówkę, bo np. skorzystałem z samo-resetowania zdarzeń i Interlocked do inicjalizacji licznika aktywnych wątków (to chyba nawet nie wymaga synchronizacji, bo i tak wszystkie wątki poboczne śpią w tym czasie i nie ruszają tej zmiennej). Tak więc kodu szablonu jest jeszcze mniej.

Z innej beczki to raczej odradzałbym używać WinApi, bo po co? Z tego co widzę to pascal ma jakieś wsparcie dla TThread.

Pytanie raczej powinno brzmieć — po co używać opasłych klas, a tym bardziej dodatkowych bibliotek, skoro system zapewnia bardzo proste w użyciu API, którym mogę zrealizować to czego potrzebuję? Zaznaczyłem na początku, że uczę się wielowątkowości i najlepiej jest się uczyć na surowym, systemowym API, czytając ichnią dokumentację.

Ostatecznie wolałbym użyć API SDL-a, żeby mieć rozwiązanie wieloplatformowe. Ale na razie chcę się pobawić Win32 API, żeby zrozumieć temat.


Drunky napisał(a):

Najbezpieczniej będzie dodawać/usuwać nowy wątek w momencie, gdy wątki poboczne aktualnie nic nie robią (czyli gdy wszystkie zakończyły działanie).

Dokładnie, i to jest gwarantowane przez architekturę mojego silnika.

Główny wątek realizuje aktualizowanie logiki gry, a wątki poboczne realizują renderowanie. W danym momencie albo działa główny wątek i aktualizuje logikę, albo trwa renderowanie w wielu wątkach pobocznych — obie te czynności dzieją się sekwencyjnie. Zmiana liczby wątków może zostać wykonana w trakcie aktualizacji logiki, a wtedy wątki renderujące zawsze oczekują, więc można je swobodnie zakańczać i dodawać nowe.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 1x, ostatnio: flowCRANE
SL
  • Rejestracja:około 7 lat
  • Ostatnio:około 5 godzin
  • Postów:890
0
flowCRANE napisał(a):

Pytanie raczej powinno brzmieć — po co używać opasłych klas, a tym bardziej dodatkowych bibliotek, skoro system zapewnia bardzo proste w użyciu API, którym mogę zrealizować to czego potrzebuję?

SDL jest przenośny, więc jest to duża wartość sama w sobie

Zaznaczyłem na początku, że uczę się wielowątkowości i najlepiej jest się uczyć na surowym, systemowym API, czytając ichnią dokumentację.

API SDL przypomina posixa i co za tym idzie: support do multithreadingu w innych językach (C++, Java, Go, Rust). IMO lepiej nauczyć się jednej abstrakcji, bo twoja wiedza będzie bardziej aplikowalna do użycia w przyszłości

Działa albo jedno, albo drugie — nigdy obie rzeczy jednocześnie.

Właśnie o to chodzi, żeby zmienić podejście. Łącząc obie operacje niwelujesz do minimum szansę, że popsujesz coś w synchronizacji

flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

Spoko, ostatecznie, we właściwym projekcie, użyję tego co oferuje SDL3 — Win32 API w nim używam tylko do tego, czego SDL nie oferuje i dla czego nie ma przenośnych implementacji. Natomiast do nauki, systemowe API jest idealne i bardzo proste w użyciu.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 1x, ostatnio: flowCRANE
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

Dobra, pobawiłem się tym wszystkim i działa cacy — dziękuję wszystkim za dyskusję.

Dla potomnych zostawię trochę przykładów, w kilku kolejnych postach. Demówkę od @Drunky maksymalnie skróciłem, tak aby mieć jak najmniej kodu do zabawy (i usunąłem Sleepa, aby mieć ~pewność, że synchronizacja działa prawidłowo), więc niżej podstawa korzystająca z podstawowych klas Pascala (praktycznie to samo co oryginalnie):

Kopiuj
uses
  SysUtils,
  Classes,
  SyncObjs;

type
  TWorkerThread = class(TThread)
  private
    FIndex: Integer;
  protected
    procedure Execute(); override;
  public
    constructor Create(AIndex: Integer);
  end;

const
  THREAD_NUM = 4;

var
  Threads:     array [0 .. THREAD_NUM - 1] of TWorkerThread;
  EventStart:  array [0 .. THREAD_NUM - 1] of TEvent;
  EventFinish: TEvent;
  ThreadsLock: TCriticalSection;
  ThreadsNum:  Integer;

  constructor TWorkerThread.Create(AIndex: Integer);
  begin
    FIndex := AIndex;
    inherited Create(False);
  end;

  procedure TWorkerThread.Execute();
  begin
    repeat
      if EventStart[FIndex].WaitFor(INFINITE) = wrSignaled then
      begin
        if Terminated then exit;

        WriteLn('Thread ', FIndex, ' finished its job.');

        ThreadsLock.Enter();
        ThreadsNum += 1;

        if ThreadsNum = THREAD_NUM then
          EventFinish.SetEvent();

        ThreadsLock.Leave();
      end;
    until False;
  end;

var
  Index:      Integer;
  IndexEvent: Integer;
begin
  for Index := 0 to THREAD_NUM - 1 do
    EventStart[Index] := TEvent.Create(nil, False, False, '');

  EventFinish := TEvent.Create(nil, False, False, '');
  ThreadsLock := TCriticalSection.Create();

  for Index := 0 to THREAD_NUM - 1 do
    Threads[Index] := TWorkerThread.Create(Index);

  for Index := 1 to 5 do
  begin
    ThreadsNum := 0;

    for IndexEvent := 0 to THREAD_NUM - 1 do
      EventStart[IndexEvent].SetEvent();

    EventFinish.WaitFor(INFINITE);
    WriteLn();
  end;

  for Index := 0 to THREAD_NUM - 1 do
  begin
    Threads[Index].Terminate();
    EventStart[Index].SetEvent();

    Threads[Index].WaitFor();
    Threads[Index].Free();

    EventStart[Index].Free();
  end;

  EventFinish.Free();
  ThreadsLock.Free();

  ReadLn();
end.

Wyjście:

Kopiuj
Thread 0 finished its job.
Thread 1 finished its job.
Thread 2 finished its job.
Thread 3 finished its job.

Thread 0 finished its job.
Thread 1 finished its job.
Thread 2 finished its job.
Thread 3 finished its job.

Thread 1 finished its job.
Thread 0 finished its job.
Thread 2 finished its job.
Thread 3 finished its job.

Thread 0 finished its job.
Thread 1 finished its job.
Thread 2 finished its job.
Thread 3 finished its job.

Thread 0 finished its job.
Thread 1 finished its job.
Thread 2 finished its job.
Thread 3 finished its job.

Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 1x, ostatnio: flowCRANE
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

A tutaj rozwiązanie we Free Pascalu, ale wykorzystujące czyste Win32 API. Nie dodawałem sprawdzania błędów, ale jeśli ktoś będzie tego potrzebował, to raczej dorzuci sobie te dodatkowe ify.

Kopiuj
uses
  Windows;

const
  THREAD_NUM = 4;

type
  PParameter = ^TParameter;
  TParameter = record
    Index:      LONG;
    Terminated: BOOL;
  end;

var
  Thread:      array [0 .. THREAD_NUM - 1] of HANDLE;
  ThreadID:    array [0 .. THREAD_NUM - 1] of DWORD;
  Parameter:   array [0 .. THREAD_NUM - 1] of TParameter;
  EventStart:  array [0 .. THREAD_NUM - 1] of HANDLE;
  EventFinish: HANDLE;
  ThreadsLock: CRITICAL_SECTION;
  ThreadsNum:  LONG;

  function ThreadFunc (AParameter: PParameter): DWORD; stdcall;
  begin
    repeat
      if WaitForSingleObject(EventStart[AParameter^.Index], INFINITE) = WAIT_OBJECT_0 then
      begin
        if AParameter^.Terminated then exit(0);

        WriteLn('Thread ', AParameter^.Index, ' finished its job.');

        EnterCriticalSection(@ThreadsLock);
        ThreadsNum += 1;

        if ThreadsNum = THREAD_NUM then
          SetEvent(EventFinish);

        LeaveCriticalSection(@ThreadsLock);
      end;
    until False;
  end;

var
  Index:      LONG;
  IndexEvent: LONG;
begin
  InitializeCriticalSection(@ThreadsLock);
  EventFinish := CreateEvent(nil, False, False, '');

  for Index := 0 to THREAD_NUM - 1 do
  begin
    EventStart[Index] := CreateEvent(nil, False, False, '');

    Parameter[Index].Index      := Index;
    Parameter[Index].Terminated := False;

    Thread[Index] := CreateThread(nil, 0, @ThreadFunc, @Parameter[Index], 0, ThreadID[Index]);
  end;

  for Index := 1 to 5 do
  begin
    ThreadsNum := 0;

    for IndexEvent := 0 to THREAD_NUM - 1 do
      SetEvent(EventStart[IndexEvent]);

    WaitForSingleObject(EventFinish, INFINITE);
    WriteLn();
  end;

  for Index := 0 to THREAD_NUM - 1 do
  begin
    Parameter[Index].Terminated := True;

    SetEvent(EventStart[Index]);
    WaitForSingleObject(Thread[Index], INFINITE);

    CloseHandle(Thread[Index]);
    CloseHandle(EventStart[Index]);
  end;

  CloseHandle(EventFinish);
  DeleteCriticalSection(@ThreadsLock);

  ReadLn();
end.

Wyjście:

Kopiuj
Thread 1 finished its job.
Thread 0 finished its job.
Thread 3 finished its job.
Thread 2 finished its job.

Thread 0 finished its job.
Thread 3 finished its job.
Thread 2 finished its job.
Thread 1 finished its job.

Thread 0 finished its job.
Thread 2 finished its job.
Thread 1 finished its job.
Thread 3 finished its job.

Thread 0 finished its job.
Thread 3 finished its job.
Thread 2 finished its job.
Thread 1 finished its job.

Thread 1 finished its job.
Thread 0 finished its job.
Thread 2 finished its job.
Thread 3 finished its job.

Efekt taki sam, ale brak bloatu. Później jeszcze przygotuję przykład używający API SDL3.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

W sumie to te przykłady da się jeszcze bardziej uprościć. Zamiast sekcji krytycznej i zliczania liczby wątków, które zakończyły obliczenia (by ustawić EventFinish i wznowić główny wątek), można zadeklarować tyle samo zdarzeń EventFinish co EventStart — czyli dwie tablice. Wątek czeka na zdarzenie startu, następnie robi robotę i ustawia swoje zdarzenie końca, w tej tablicy. Natomiast wątek główny czeka na to aż wszystkie zdarzenia końca zostaną ustawione.

W przypadku Win32 API wystarczy WaitForMultipleObjects — zarówno do wznowienia wątku głównego, jak i do niszczenia ich wszystkich, w trakcie finalizacji zasobów. Czyli robimy dwie tablice zdarzeń:

Kopiuj
var
  EventStart:  array [0 .. THREAD_NUM - 1] of HANDLE;
  EventFinish: array [0 .. THREAD_NUM - 1] of HANDLE;

Inicjalizujemy je tworząc zdarzenia samoresetujące (bo idealnie do tego celu pasują):

Kopiuj
for Index := 0 to THREAD_NUM - 1 do
begin
  {...}

  EventStart[Index]  := CreateEvent(nil, False, False, '');
  EventFinish[Index] := CreateEvent(nil, False, False, '');

  {...}
end;

Żeby w głównej pętli zasygnalizować wątkom pobocznym robotę do zrobienia, ustawia się w pętli wszystkie zdarzenia startu:

Kopiuj
for IndexEvent := 0 to THREAD_NUM - 1 do
  SetEvent(EventStart[IndexEvent]);

natomiast aby poczekać aż wszystkie skończą robotę, wystarczy sprawdzić stan wszystkich zdarzeń końca:

Kopiuj
WaitForMultipleObjects(THREAD_NUM, @EventFinish, True, INFINITE);

Czyli całe przełączanie się pomiędzy wątkiem głównym a pobocznymi to całe trzy linijki kodu. Natomiast funkcja-workera też się mocno upraszcza, bo zamiast liczenia wątków, wystarczy tylko ustawić zdarzenie końca, odpowiednie dla danego wątku:

Kopiuj
function ThreadFunc (AParameter: PParameter): DWORD; stdcall;
begin
  repeat
    if WaitForSingleObject(EventStart[AParameter^.Index], INFINITE) = WAIT_OBJECT_0 then
    begin
      if AParameter^.Terminated then exit(0);

      WriteLn('Thread ', AParameter^.Index, ' finished its job.');
      SetEvent(EventFinish[AParameter^.Index]);
    end;
  until False;
end;

Natomiast nie ma do tego zamiennika w przypadku klas Free Pascala — nie ma żadnej funkcji/metody przyjmującej listę obiektów TEvent, więc pozostaje czekanie na nie wszystkie w pętli, każdemu zdarzeniu z tablicy EventFinish wywołując metodę WaitFor (tak jak to na początku robiłem z sekcjami krytycznymi). Spokojnie można to wykonać w pętli, bo nie ma znaczenia kolejność sygnalizacji zdarzeń końca (czyli to który wątek kiedy skończy robotę), bo tak czy siak taka pętla zakończy działanie dopiero kiedy wszystkie zdarzenia końca będą sygnalizowane.

Nieważne, cały kod nowego testera jest tutaj:

Kopiuj
uses
  Windows;

const
  THREAD_NUM = 4;

type
  PParameter = ^TParameter;
  TParameter = record
    Index:      LONG;
    Terminated: BOOL;
  end;

var
  Thread:      array [0 .. THREAD_NUM - 1] of HANDLE;
  ThreadID:    array [0 .. THREAD_NUM - 1] of DWORD;
  Parameter:   array [0 .. THREAD_NUM - 1] of TParameter;
  EventStart:  array [0 .. THREAD_NUM - 1] of HANDLE;
  EventFinish: array [0 .. THREAD_NUM - 1] of HANDLE;

  function ThreadFunc (AParameter: PParameter): DWORD; stdcall;
  begin
    repeat
      if WaitForSingleObject(EventStart[AParameter^.Index], INFINITE) = WAIT_OBJECT_0 then
      begin
        if AParameter^.Terminated then exit(0);

        WriteLn('Thread ', AParameter^.Index, ' finished its job.');
        SetEvent(EventFinish[AParameter^.Index]);
      end;
    until False;
  end;

var
  Index:      LONG;
  IndexEvent: LONG;
begin
  for Index := 0 to THREAD_NUM - 1 do
  begin
    Parameter[Index].Index      := Index;
    Parameter[Index].Terminated := False;

    EventStart[Index]  := CreateEvent(nil, False, False, '');
    EventFinish[Index] := CreateEvent(nil, False, False, '');

    Thread[Index] := CreateThread(nil, 0, @ThreadFunc, @Parameter[Index], 0, ThreadID[Index]);
  end;

  for Index := 1 to 5 do
  begin
    for IndexEvent := 0 to THREAD_NUM - 1 do
      SetEvent(EventStart[IndexEvent]);

    WaitForMultipleObjects(THREAD_NUM, @EventFinish, True, INFINITE);
    WriteLn();
  end;

  for Index := 0 to THREAD_NUM - 1 do
  begin
    Parameter[Index].Terminated := True;
    SetEvent(EventStart[Index]);
  end;

  WaitForMultipleObjects(THREAD_NUM, @Thread, True, INFINITE);

  for Index := 0 to THREAD_NUM - 1 do
  begin
    CloseHandle(Thread[Index]);
    CloseHandle(EventStart[Index]);
    CloseHandle(EventFinish[Index]);
  end;

  ReadLn();
end.

Co ciekawe, SDL3 również nie posiada wieloplatformowej opcji używającej czegoś podobnego i nie daje prostej możliwości czekania na wiele conditional variables, w dodatku oczekiwanie bazuje na muteksach, a więc lockach o zakresie globalnym (między procesami), więc to też trochę armata na muchę.

Spróbuję naskrobać tester w SDL3, ale coś mi się wydaje, że ostatecznie skorzystam z Win32 API w docelowym projekcie, a dla innych platform w przyszłości napiszę osobne, dedykowane danej platformie rozwiązanie. Bo z tego wszystkiego to akurat najprostsze rozwiązanie, wymagające najmniej kodu, to akurat czyste Win32 API, którego tak bardzo się wszyscy boją. 😉


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

A tutaj jest to skrócone rozwiązanie z użyciem klas Free Pascala:

Kopiuj
uses
  SysUtils,
  Classes,
  SyncObjs;

type
  TWorkerThread = class(TThread)
  private
    FIndex: Integer;
  protected
    procedure Execute(); override;
  public
    constructor Create(AIndex: Integer);
  end;

const
  THREAD_NUM = 4;

var
  Threads:     array [0 .. THREAD_NUM - 1] of TWorkerThread;
  EventStart:  array [0 .. THREAD_NUM - 1] of TEvent;
  EventFinish: array [0 .. THREAD_NUM - 1] of TEvent;

  constructor TWorkerThread.Create(AIndex: Integer);
  begin
    FIndex := AIndex;
    inherited Create(False);
  end;

  procedure TWorkerThread.Execute();
  begin
    repeat
      EventStart[FIndex].WaitFor(INFINITE);

      if not Terminated then
      begin
        WriteLn('Thread ', FIndex, ' finished its job.');
        EventFinish[FIndex].SetEvent();
      end;
    until Terminated;
  end;

var
  Index:      Integer;
  IndexEvent: Integer;
begin
  for Index := 0 to THREAD_NUM - 1 do
  begin
    EventStart[Index]  := TEvent.Create(nil, False, False, '');
    EventFinish[Index] := TEvent.Create(nil, False, False, '');

    Threads[Index] := TWorkerThread.Create(Index);
  end;

  for Index := 1 to 5 do
  begin
    for IndexEvent := 0 to THREAD_NUM - 1 do
      EventStart[IndexEvent].SetEvent();

    for IndexEvent := 0 to THREAD_NUM - 1 do
      EventFinish[IndexEvent].WaitFor(INFINITE);

    WriteLn();
  end;

  for Index := 0 to THREAD_NUM - 1 do
  begin
    Threads[Index].Terminate();
    EventStart[Index].SetEvent();

    Threads[Index].WaitFor();
    Threads[Index].Free();

    EventStart[Index].Free();
    EventFinish[Index].Free();
  end;

  ReadLn();
end.

Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 2x, ostatnio: flowCRANE
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
0

I ostatni przykład, tym razem używający funkcji SDL3. Dużo tego kodu, bo zamiast jednego zdarzenia per wątek, trzeba mieć po trzy zmienne — flagę logiczną, conditional variable i mutex. No i trzeba pamiętać, że wątku się nie zwalnia ręcznie — odpala się SDL_WaitThread i czeka aż funkcja workera zakończy działanie, a sprzątaniem zajmuje się SDL.

W razie czego, korzystałem z nagłówków Lazarus-SDL-3.0-Packages-and-Examples, a implementację oparłem na przykładzie podanym w dokumentacji funkcji SDL_CreateCondition.

Kopiuj
uses
  SDL3;

const
  THREAD_NUM = 4;

type
  PThreadData = ^TThreadData;
  TThreadData = record
    Index:      Integer;
    Terminated: Boolean;
  end;

var
  Thread:          array [0 .. THREAD_NUM - 1] of PSDL_Thread;
  ThreadData:      array [0 .. THREAD_NUM - 1] of TThreadData;

  StartCondition:  array [0 .. THREAD_NUM - 1] of PSDL_Condition;
  StartMutex:      array [0 .. THREAD_NUM - 1] of PSDL_Mutex;
  StartFlag:       array [0 .. THREAD_NUM - 1] of Boolean;

  FinishCondition: array [0 .. THREAD_NUM - 1] of PSDL_Condition;
  FinishMutex:     array [0 .. THREAD_NUM - 1] of PSDL_Mutex;
  FinishFlag:      array [0 .. THREAD_NUM - 1] of Boolean;

  function ThreadFunc (AData: PThreadData): Integer; cdecl;
  begin
    repeat
      SDL_LockMutex(StartMutex[AData^.Index]);

      while not StartFlag[AData^.Index] do
        SDL_WaitCondition(StartCondition[AData^.Index], StartMutex[AData^.Index]);

      StartFlag[AData^.Index] := False;
      SDL_UnlockMutex(StartMutex[AData^.Index]);

      if not AData^.Terminated then
      begin
        WriteLn('Thread ', AData^.Index, ' finished its job.');

        SDL_LockMutex(FinishMutex[AData^.Index]);
        FinishFlag[AData^.Index] := True;

        SDL_SignalCondition(FinishCondition[AData^.Index]);
        SDL_UnlockMutex(FinishMutex[AData^.Index]);
      end;
    until AData^.Terminated;

    Result := 0;
  end;

var
  Index:     Integer;
  IndexLoop: Integer;
begin
  for Index := 0 to THREAD_NUM - 1 do
  begin
    ThreadData[Index].Index      := Index;
    ThreadData[Index].Terminated := False;

    StartCondition[Index]  := SDL_CreateCondition();
    StartMutex[Index]      := SDL_CreateMutex();
    StartFlag[Index]       := False;

    FinishCondition[Index] := SDL_CreateCondition();
    FinishMutex[Index]     := SDL_CreateMutex();
    FinishFlag[Index]      := False;

    Thread[Index] := SDL_CreateThreadRuntime(TSDL_ThreadFunction(@ThreadFunc), '', @ThreadData[Index], nil, nil);
  end;

  for IndexLoop := 1 to 5 do
  begin
    for Index := 0 to THREAD_NUM - 1 do
    begin
      SDL_LockMutex(StartMutex[Index]);
      StartFlag[Index] := True;

      SDL_SignalCondition(StartCondition[Index]);
      SDL_UnlockMutex(StartMutex[Index]);
    end;

    for Index := 0 to THREAD_NUM - 1 do
    begin
      SDL_LockMutex(FinishMutex[Index]);

      while not FinishFlag[Index] do
        SDL_WaitCondition(FinishCondition[Index], FinishMutex[Index]);

      FinishFlag[Index] := False;
      SDL_UnlockMutex(FinishMutex[Index]);
    end;

    WriteLn();
  end;

  for Index := 0 to THREAD_NUM - 1 do
  begin
    SDL_LockMutex(StartMutex[Index]);

    ThreadData[Index].Terminated := True;
    StartFlag[Index] := True;

    SDL_SignalCondition(StartCondition[Index]);
    SDL_UnlockMutex(StartMutex[Index]);

    SDL_WaitThread(Thread[Index], nil);
    SDL_DetachThread(Thread[Index]);

    SDL_DestroyMutex(StartMutex[Index]);
    SDL_DestroyMutex(FinishMutex[Index]);
    SDL_DestroyCondition(StartCondition[Index]);
    SDL_DestroyCondition(FinishCondition[Index]);
  end;

  ReadLn();
end.

This shit is crazy… tęsknię za Win32 API… 🤣

No nic, w każdym razie działa tak samo jak tamte inne przykłady, no i jest to rozwiązanie przenośne na wszystkie platformy wspierane przez SDL3. Myślę, że na tym mogę poprzestać — rozgryzłem temat i już wiem wszystko co chciałem się dowiedzieć. Jeszcze raz dzięki za dyskusję.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 5x, ostatnio: flowCRANE
SL
Mi sie podoba xd Chyba zależy od preferencji
flowCRANE
Jest spoko, ale najważniejsze że działa prawidłowo. ;)
flowCRANE
Moderator Delphi/Pascal
  • Rejestracja:ponad 13 lat
  • Ostatnio:około 3 godziny
  • Lokalizacja:Tuchów
  • Postów:12171
4

Zrobiłem demówkę do testowania tego co ostatecznie chcę zaimplementować w swoim silniku, czyli wielowątkowego software'owego renderera. Jego zadaniem jest uzupełnienie bufora klatki (zwykła tablica) w dane kolorów, gdzie każdy wątek zajmuje się "renderowaniem" swojego zestawu scanline'ów. Takie renderowanie wykonywane jest po stronie CPU, w zadanej liczbie wątków pobocznych. Po zakończeniu uzupełniania bufora, jego zawartość kopiowana jest do tekstury znajdującej się po stronie GPU i dalej renderowane są inne pierdy, już używając renderera SDL-a i przyspieszenia sprzętowego.

Demówka renderuje deseń oraz krążący w kółko kwadrat. Na belce tytułowej okna, co sekundę aktualizowane są informacje na temat liczby działających wątków renderujących, a także średni framerate oraz czas renderowania klatki, wyrażony w milisekundach, coby wiedzieć jak liczba wątków wpływa na czas generowania klatek.

screenshot-20250222192601.gif

Instrukcja obsługi:

Klawisz Funkcja
Up zwiększ liczbę wątków renderujących o 1.
Down zmniejsz liczbę wątków renderujących o 1.
PageUp zwiększ liczbę wątków renderujących o 4.
PageDown zmniejsz liczbę wątków renderujących o 4.
Esc zamknij program.

Niżej znajduje się pełen kod źródłowy z opisami, a w załącznikach jest archiwum z pełnymi źródłami, również z nagłówkami oraz biblioteką DLL SDL-a. Wystarczy otworzyć projekt w Lazarusie, skompilować i uruchomić.

Kopiuj
uses
  SDL3,
  MultiSignal;

const
  // Rozmiar bufora klatki, uzupełnianego w dane pikseli w wątkach renderujących.
  FRAME_W = 256;
  FRAME_H = 240;

const
  // Maksymalna liczba wątków, nie większa niż liczba scanline'ów bufora klatki.
  THREAD_NUM_MAX = FRAME_H;

const
  // Podstawowe dane dotyczące ruchomego kwadratu.
  SQUARE_CENTER_X = FRAME_W div 2; // Środek bufora klatki, w poziomie.
  SQUARE_CENTER_Y = FRAME_H div 2; // Środek bufora klatki, w pionie.
  SQUARE_SIZE     = 32; // Rozmiar kwadratu, jako połowa jego wysokości/szerokości.
  SQUARE_RADIUS   = FRAME_H - SQUARE_CENTER_X - SQUARE_SIZE - 8; // Promień zataczanego kręgu.

type
  // Pakiet danych, do których ma dostęp funkcja workera w dostarczanym parametrze.
  PThreadData = ^TThreadData;
  TThreadData = record
    Enter: TSignal; // Obiekt sygnału rozpoczęcia renderowania.
    Leave: TSignal; // Obiekt sygnału zakończenia renderowania.
    Index: Integer; // Indeks wątku, określa początkowy indeks scanline do renderowania.
    Ended: Boolean; // Flaga informująca wątek worker o tym, że ma zakończyć działanie.
  end;

var
  ThreadNum:        Integer; // Aktualna liczba wątków renderujących, określa też offset scanline'nów dla danego workera.
  ThreadNumChanged: Boolean = False; // Flaga określająca czy liczba wątków się zmieniła i należy zaktualizować tytuł okna.
  ThreadFunc:       array [0 .. THREAD_NUM_MAX - 1] of PSDL_Thread; // Tablica uchwytów wątków renderujących.
  ThreadData:       array [0 .. THREAD_NUM_MAX - 1] of TThreadData; // Tablica pakietów danych dla wątków renderujących.

var
  SquareAngle: Single = 0.0; // Aktualny kąt używany do obliczenia pozycji kwadratu.
  SquareX:     Integer; // Aktualna współrzędna X (wyznacza środek kwadratu).
  SquareY:     Integer; // Aktualna współrzędna Y (wyznacza środek kwadratu).

var
  // Bufor klatki, uzupełniany przez wątki renderujące w dane pikseli dla aktualnej klatki.
  FrameBuffer: array [0 .. FRAME_H - 1, 0 .. FRAME_W - 1] of record A, R, G, B: UInt8 end;

  // Funkcja renderująca.
  function ThreadWorker (AThreadData: PThreadData): Integer; cdecl;
  var
    IndexScanline: Integer;
    IndexPixel:    Integer;
  begin
    repeat
      // Poczekaj aż główny wątek da sygnał do renderowania.
      SignalWait(@AThreadData^.Enter);

      // Jeśli sygnał nie dotyczy zakończenia pracy wątku, wyrenderuj scanline'y dla nowej klatki.
      if not AThreadData^.Ended then
      begin
        // Rozpocznij renderowanie od scanline o indeksie równym indeksowi wątku.
        IndexScanline := AThreadData^.Index;

        // W pętli renderuj wszystkie scanline'y przrypisane do tego wątku.
        while IndexScanline < FRAME_H do
        begin
          // Wypełnij cały wiersz aktualnej scanline w dane pikseli.
          for IndexPixel := 0 to FRAME_W - 1 do
          begin
            FrameBuffer[IndexScanline, IndexPixel].R := IndexScanline xor IndexPixel;
            FrameBuffer[IndexScanline, IndexPixel].G := IndexScanline xor IndexPixel;
            FrameBuffer[IndexScanline, IndexPixel].B := IndexScanline xor IndexPixel;
          end;

          // Jeśli w tej scanline znajduje się ruchomy kwadrat, wyrenderuj jego fragment.
          if (IndexScanline > SquareY - SQUARE_SIZE) and (IndexScanline < SquareY + SQUARE_SIZE) then
            for IndexPixel := SquareX - SQUARE_SIZE + 1 to SquareX + SQUARE_SIZE - 1 do
            begin
              FrameBuffer[IndexScanline, IndexPixel].R := not FrameBuffer[IndexScanline, IndexPixel].R;
              FrameBuffer[IndexScanline, IndexPixel].G := not FrameBuffer[IndexScanline, IndexPixel].G;
              FrameBuffer[IndexScanline, IndexPixel].B := not FrameBuffer[IndexScanline, IndexPixel].B;
            end;

          // Przejdź do kolejnej scanlini, pomijając te wypełniane przez pozostałe wątki renderujące.
          IndexScanline += ThreadNum;
        end;

        // Koniec rendeorowania scanlinii, daj sygnał głównemu wątkowi i wróć do oczekiwania na kolejny sygnał startu.
        SignalEmit(@AThreadData^.Leave);
      end;
    until AThreadData^.Ended;

    // Kod zwracany przez wątek w trakcie jego niszczenia nie jest używany w tej demówce, więc zwróć cokolwiek.
    Result := 0;
  end;

var
  Title:        array [0 .. 127] of Char; // Bufor znaków na potrzeby formatowania ciągu z tytułem okna.
  Window:       PSDL_Window;   // Kontekst okna SDL-a.
  Renderer:     PSDL_Renderer; // Kontekst renderera SDL-a.
  Texture:      PSDL_Texture;  // Tekstura, do której kopiowana jest zawartość bufora klatki i potem renderowana w oknie.
  TextureData:  Pointer;       // W trakcie blokowania teksty, pointer ten wskazuje na blok danych pikseli tekstury.
  TexturePitch: Integer;       // Rozmiar wiersza streamowanej tekstury (nieużywany, ale wymagany do zablokowania tekstury).

  // Funkcja aktualizująca liczbę wątków renderujących.
  procedure ThreadNumUpdate (ANum: Integer);
  var
    Index: Integer;
  begin
    // Dotnij liczbę wątków do obsługiwanego zakresu i wyjdź, jeśli liczba wątków się nie zmienia.
    if ANum > THREAD_NUM_MAX then ANum := THREAD_NUM_MAX;
    if ANum < 1              then ANum := 1;
    if ANum = ThreadNum      then exit;

    // Jeśli liczba wątków ma być zwiększona, więc stwórz i dodaj nowe na koniec aktualnej puli.
    if ANum > ThreadNum then
      for Index := ThreadNum to ANum - 1 do
      begin
        // Stwórz obiekty sygnałów startu i końca renderowania (condition, mutex i flaga ligiczna).
        SignalInitialize(@ThreadData[Index].Enter);
        SignalInitialize(@ThreadData[Index].Leave);

        // Ustaw indeks wątku, tak aby wątki w puli miały indeksy rosnące, bez żadnych przerw.
        ThreadData[Index].Index := Index;
        ThreadData[Index].Ended := False;

        // Stwórz i uruchom nowy wątek renderujący, przekazując mu wskaźnik na pakiet danych.
        ThreadFunc[Index] := SDL_CreateThreadRuntime(TSDL_ThreadFunction(@ThreadWorker), '', @ThreadData[Index], nil, nil);
      end
    else
      // Liczba wątków ma być zmniejszona, więc usuń określoną ich liczbę z końca puli.
      for Index := ThreadNum - 1 downto ANum do
      begin
        // Ustaw flagę zakończenia działania wątku i wyślij mu sygnał, aby się wybudził.
        ThreadData[Index].Ended := True;
        SignalEmit(@ThreadData[Index].Enter);

        // Poczekaj aż wątek wybudzi się, wyjdzie z pętli i zakończy działanie, a następnie go usuń.
        SDL_WaitThread(ThreadFunc[Index], nil);
        SDL_DetachThread(ThreadFunc[Index]);

        // Usuń przypisane do tego wątku obiekty sygnałów startu i zakończenia renderowania (condition, mutex i flaga).
        SignalFinalize(@ThreadData[Index].Enter);
        SignalFinalize(@ThreadData[Index].Leave);
      end;

    // Ustaw nową liczbę wątków renderujących i zapal flagę, tak aby w tej klatce tytuł okna został odświeżony.
    ThreadNum        := ANum;
    ThreadNumChanged := True;
  end;

label
  0001;
var
  Event:        TSDL_Event;    // Do przetwarzania zdarzeń SDL-a, w głównej pętli.
  FrameRate:    Integer = 0;   // Ostatni wynik pomiarów liczby generowanych klatek na sekundę (do wyświetlenia na belce).
  FrameRateNew: Integer = 0;   // Aktualna liczba wygenerowanych klatek.
  FrameTime:    Single  = 0.0; // Ostatni wynik pomiaru czasu genenrowania klatki, w milisekundach.
  TimeSample:   UInt64;        // Timestamp pobierany przed pomiarem czasu renderowania klatki.
  Second:       UInt64;        // Ostatnio określony numer sekundy, na potrzeby odświeżania tytułu okna.
  SecondNew:    UInt64;        // Aktualny numer sekundy, do wykrycia zmiany sekundy i konieczności odświeżenia tytułu.
  Index:        Integer;       // Do indeksowania pętli w głównym bloku.
begin
  SDL_Init(SDL_INIT_VIDEO);

  // Wypełni bufor wartościami "255", aby nie musieć modyfikować kanałów alpha w wątkach renderujących.
  FillChar(FrameBuffer, SizeOf(FrameBuffer), $FF);
  Second := SDL_GetTicks() div 1000; // Pobierz aktualny numer sekundy.

  // Zainicjalizuj tytuł okna domyślnymi wartościami.
  SDL_snprintf(@Title, Length(Title), 'Threads: %d/%d — Frame rate: %d — Frame time: %.2fms', [ThreadNum, THREAD_NUM_MAX, FrameRate, FrameTime]);

  // Stwórz okno, renderer i teksturę do streamowania bufora klatki oraz określ liczbę wątków zgodną z liczbą rdzeni CPU.
  Window    := SDL_CreateWindow   (@Title, 640, 480, SDL_WINDOW_RESIZABLE);
  Renderer  := SDL_CreateRenderer (Window, 'opengl');
  Texture   := SDL_CreateTexture  (Renderer, SDL_PIXELFORMAT_BGRA8888, SDL_TEXTUREACCESS_STREAMING, FRAME_W, FRAME_H);
  ThreadNum := SDL_GetNumLogicalCPUCores();

  // Stwórz wątki i sygnały dla początkowej liczby wątków renderujących.
  for Index := 0 to ThreadNum - 1 do
  begin
    SignalInitialize(@ThreadData[Index].Enter);
    SignalInitialize(@ThreadData[Index].Leave);

    ThreadData[Index].Index := Index;
    ThreadData[Index].Ended := False;

    ThreadFunc[Index] := SDL_CreateThreadRuntime(TSDL_ThreadFunction(@ThreadWorker), '', @ThreadData[Index], nil, nil);
  end;

  // Główna pętla
  while True do
  begin
    ThreadNumChanged := False;

    // Przetwórz wszystkie zdarzenia z kolejki zdarzeń SDL-a.
    while SDL_PollEvent(@Event) do
    case Event._Type of

      // Jeśli wciśnięto klawisz (lub jest trzymany), zaktualizuj liczbę wątków lub zakończ działanie programu.
      SDL_EVENT_KEY_DOWN:
      case Event.Key.Scancode of
        SDL_SCANCODE_UP:       ThreadNumUpdate(ThreadNum + 1);
        SDL_SCANCODE_DOWN:     ThreadNumUpdate(ThreadNum - 1);
        SDL_SCANCODE_PAGEUP:   ThreadNumUpdate(ThreadNum + 4);
        SDL_SCANCODE_PAGEDOWN: ThreadNumUpdate(ThreadNum - 4);
        SDL_SCANCODE_ESCAPE:   goto 0001;
      end;

      SDL_EVENT_QUIT: goto 0001;
    end;

    // Logika — zaktualizuj pozycję ruchomego kwadratu.
    SquareAngle += (2 * Pi) / (60 * 2);
    SquareX     := Round(SQUARE_CENTER_X + Cos(SquareAngle) * SQUARE_RADIUS);
    SquareY     := Round(SQUARE_CENTER_Y + Sin(SquareAngle) * SQUARE_RADIUS);

    // Renderowanie — pobierz aktualny czas, do pomiarów wydajności renderowania.
    TimeSample  := SDL_GetTicksNS();
    begin
      // Wyślij sygnał wszystkim wątkom, aby się wybudziły i rozpoczęły renderowanie bieżącej klatki.
      for Index := 0 to ThreadNum - 1 do
        SignalEmit(@ThreadData[Index].Enter);

      // Poczekaj aż wszystkie wątki zakończą renderowanie i przejdą w stan uśpienia.
      for Index := 0 to ThreadNum - 1 do
        SignalWait(@ThreadData[Index].Leave);

      // Wyczyść okno.
      SDL_SetRenderDrawColor(Renderer, 0, 0, 0, 255);
      SDL_RenderClear(Renderer);

      // Zablokuj teksturę i uzyskaj wskaźnik na blok jej danych. Kiedy jest zablokowana, skopiuj do pozyskanego bloku
      // zawartość bufora klatki, wyrenderowanego przez wątki. Na koniec odblokuj teksturę, aby wysłać dane do GPU.
      SDL_LockTexture(Texture, nil, @TextureData, @TexturePitch);
      Move(FrameBuffer, TextureData^, SizeOf(FrameBuffer));
      SDL_UnlockTexture(Texture);

      // Wyrenderuj zaktualizowaną teskturę klatki w oknie (przykrywając cały obszar klienta okna).
      SDL_RenderTexture(Renderer, Texture, nil, nil);
    end;
    FrameTime := (SDL_GetTicksNS() - TimeSample) / 1000000;

    // Po zakończeniu pomiaru wydajności renderowania, zaktualizuj wyrenderuj nowe dane na ekranie.
    SDL_RenderPresent(Renderer);

    // Zaktualizuj licznik framerate'u i czasu renderowania klatki.
    FrameRateNew += 1;
    SecondNew    := SDL_GetTicks() div 1000;

    if ThreadNumChanged or (SecondNew <> Second) then
    begin
      if SecondNew <> Second then
      begin
        Second       := SecondNew;
        FrameRate    := FrameRateNew;
        FrameRateNew := 0;
      end;

      SDL_snprintf(@Title, Length(Title), 'Threads: %d/%d — Frame rate: %d — Frame time: %.2fms', [ThreadNum, THREAD_NUM_MAX, FrameRate, FrameTime]);
      SDL_SetWindowTitle(Window, @Title);

      ThreadNumChanged := False;
    end;

    // To nie ma być precyzyjne, a jedynie dawać coś około 60fps.
    SDL_Delay(15);
  end;

0001:
  // Zwolnij wszystkie stworzone i aktualnie działające wątki renderujące.
  for Index := 0 to ThreadNum - 1 do
  begin
    // Ustaw flagę zakońćzenia pracy wątku i wyślij mu sygnał, że ma zakończyć działanie.
    ThreadData[Index].Ended := True;
    SignalEmit(@ThreadData[Index].Enter);

    // Poczekaj aż wątek zakończy działanie i go zwolnij z pamięci.
    SDL_WaitThread(ThreadFunc[Index], nil);
    SDL_DetachThread(ThreadFunc[Index]);

    // Zniszcz obiekty sygnałów (condition, mutex i flaga).
    SignalFinalize(@ThreadData[Index].Enter);
    SignalFinalize(@ThreadData[Index].Leave);
  end;

  // Posprzątaj i wyjdź.
  SDL_DestroyRenderer(Renderer);
  SDL_DestroyWindow(Window);
  SDL_DestroyTexture(Texture);
  SDL_Quit();
end.

Oraz moduł z implementacją obiektu sygnału:

Kopiuj
unit MultiSignal;

interface

uses
  SDL3;


type
  PSignal = ^TSignal;
  TSignal = record
    Condition: PSDL_Condition;
    Mutex:     PSDL_Mutex;
    Flag:      Boolean;
  end;


  procedure SignalInitialize (ASignal: PSignal);
  procedure SignalFinalize   (ASignal: PSignal);

  procedure SignalEmit       (ASignal: PSignal);
  procedure SignalWait       (ASignal: PSignal);


implementation


procedure SignalInitialize (ASignal: PSignal);
begin
  ASignal^.Condition := SDL_CreateCondition();
  ASignal^.Mutex     := SDL_CreateMutex();
  ASignal^.Flag      := False;
end;


procedure SignalFinalize (ASignal: PSignal);
begin
  SDL_DestroyCondition (ASignal^.Condition);
  SDL_DestroyMutex     (ASignal^.Mutex);
end;


procedure SignalEmit(ASignal: PSignal);
begin
  SDL_LockMutex(ASignal^.Mutex);

  ASignal^.Flag := True;

  SDL_SignalCondition(ASignal^.Condition);
  SDL_UnlockMutex(ASignal^.Mutex);
end;


procedure SignalWait (ASignal: PSignal);
begin
  SDL_LockMutex(ASignal^.Mutex);

  while not ASignal^.Flag do
    SDL_WaitCondition(ASignal^.Condition, ASignal^.Mutex);

  ASignal^.Flag := False;
  SDL_UnlockMutex(ASignal^.Mutex);
end;


end.

Sprawdzania błędów nie dodawałem, żeby ograniczyć tester do minimum.


Pracuję nad własną, arcade'ową, docelowo komercyjną grą z gatunku action/adventure w stylu retro (pixel art), programując silnik i powłokę gry od zupełnych podstaw, przy użyciu Free Pascala i SDL3. Więcej informacji znajdziesz na moim mikroblogu.
edytowany 2x, ostatnio: flowCRANE
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)