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
- Wątek główny zawsze zajmuje się uruchomieniem puli wątków pobocznych.
- 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ń.
- 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.
- 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.
- Ma być możliwość zmiany liczby wątków pobocznych w runtime.
- Wszystkie wątki, jeśli muszą czekać, muszą być zamrożone (żadne spinlocki gotujące CPU).
- Zamrożenie wątku nie może bazować na czymś pokroju pętli ze
Sleep(1)
, bo spadnie precyzja i wydajność procesu.
Zapewnienia
- 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ń).
- 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.
- 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.
- rozkaz wykonania kolejnych obliczeń:
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.