Tworzę prosty silnik do gier 2D i chcę zaktualizować i renderować duszki w różnych wątkach, aby dowiedzieć się, jak to zrobić.
Muszę zsynchronizować wątek aktualizacji i renderowania. Obecnie używam dwóch flag atomowych. Przepływ pracy wygląda mniej więcej tak:
Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done
W tej konfiguracji ograniczam FPS wątku renderującego do FPS wątku aktualizacji. Poza tym używam sleep()
do ograniczenia zarówno renderowania, jak i aktualizacji FPS wątku do 60, więc dwie funkcje oczekiwania nie będą długo czekać.
Problemem jest:
Średnie użycie procesora wynosi około 0,1%. Czasami nawet do 25% (w czterordzeniowym komputerze PC). Oznacza to, że wątek czeka na drugi, ponieważ funkcja oczekiwania jest pętlą while z funkcją testowania i ustawiania, a pętla while zużywa wszystkie zasoby procesora.
Moje pierwsze pytanie brzmi: czy istnieje inny sposób synchronizacji dwóch wątków? Zauważyłem, że std::mutex::lock
nie używaj procesora, gdy czeka on na zablokowanie zasobu, więc nie jest to pętla while. Jak to działa? Nie mogę użyć, std::mutex
ponieważ będę musiał zablokować je w jednym wątku i odblokować w innym wątku.
Drugie pytanie brzmi; skoro program działa zawsze z prędkością 60 klatek na sekundę, dlaczego czasami jego użycie procesora skacze do 25%, co oznacza, że jedno z dwóch czekań dużo czeka? (oba wątki są ograniczone do 60 klatek na sekundę, więc idealnie nie będą wymagały dużo synchronizacji).
Edycja: Dzięki za wszystkie odpowiedzi. Najpierw chcę powiedzieć, że nie zaczynam nowego wątku dla każdej ramki do renderowania. Zaczynam zarówno aktualizację, jak i renderowanie na początku. Myślę, że wielowątkowość może zaoszczędzić trochę czasu: Mam następujące funkcje: FastAlg () i Alg (). Alg () to zarówno moja aktualizacja obj, jak i obiekt render, a Fastalg () to moja „kolejka wysyłania renderowania do” renderer ”. W jednym wątku:
Alg() //update
FastAgl()
Alg() //render
W dwóch wątkach:
Alg() //update while Alg() //render last frame
FastAlg()
Więc może wielowątkowość może zaoszczędzić ten sam czas. (w rzeczywistości robi to w prostej aplikacji matematycznej, gdzie alg jest długim algorytmem, a szybszym - szybszym)
Wiem, że sen nie jest dobrym pomysłem, chociaż nigdy nie miałem problemów. Czy to będzie lepsze?
While(true)
{
If(timer.gettimefromlastcall() >= 1/fps)
Do_update()
}
Ale będzie to nieskończona pętla while, która będzie wykorzystywała cały procesor. Czy mogę użyć trybu uśpienia (liczba <15), aby ograniczyć użycie? W ten sposób będzie działać na przykład przy 100 fps, a funkcja aktualizacji zostanie wywołana tylko 60 razy na sekundę.
Aby zsynchronizować dwa wątki, użyję funkcji waitforsingleobject z createSemaphore, aby móc blokować i odblokowywać różne wątki (bez użycia pętli while), prawda?
źródło
Odpowiedzi:
W przypadku prostego silnika 2D ze spritami podejście jednowątkowe jest całkowicie dobre. Ale ponieważ chcesz nauczyć się wielowątkowości, powinieneś nauczyć się robić to poprawnie.
Nie rób
sleep
do kontrolowania liczby klatek na sekundę. Nigdy. Jeśli ktoś ci każe, uderz go.Po pierwsze, nie wszystkie monitory pracują z częstotliwością 60 Hz. Po drugie, dwa liczniki tykające z tą samą prędkością biegną obok siebie zawsze w końcu się zsynchronizują (upuść dwie piłki pingpongowe na stół z tej samej wysokości i słuchaj). Po trzecie, z założenia nie
sleep
jest ani dokładne, ani niezawodne. Ziarnistość może być tak mała jak 15,6 ms (w rzeczywistości domyślna w systemie Windows [1] ), a ramka ma tylko 16,6 ms przy 60 klatkach na sekundę, co pozostawia zaledwie 1 ms na wszystko inne. Ponadto trudno jest uzyskać 16,6 wielokrotności 15,6 ... Można także (i czasem będzie) powracać dopiero po 30 lub 50 lub 100 ms, a nawet dłużej.sleep
std::mutex
do powiadamiania o innym wątku. Nie po to jest.Zrobić
sleep
[2] . Ponadto cykliczny licznik czasu prawidłowo uwzględnia czas (w tym czas, który mija między nimi), podczas gdy sen przez 16,6 ms (lub 16,6 ms minus zmierzony_czas_elastyczny) nie.std::mutex
aby mieć tylko jeden wątek na raz dostęp do zasobu („wzajemnie się wykluczaj”) i aby zachować dziwną semantykęstd::condition_variable
.std::condition_variable
aby zablokować inny wątek, dopóki jakiś warunek nie będzie spełniony. Semantykastd::condition_variable
z tym dodatkowym muteksem jest wprawdzie dość dziwna i pokręcona (głównie z powodów historycznych odziedziczonych po wątkach POSIX), ale zmienna warunkowa jest poprawną operacją podstawową do tego, czego chcesz.Jeśli uważasz, że jest ci
std::condition_variable
zbyt dziwnie, aby czuć się komfortowo, możesz po prostu użyć zdarzenia Windows (nieco wolniej) lub, jeśli jesteś odważny, zbudować własne proste wydarzenie wokół NtKeyedEvent (obejmuje przerażające rzeczy na niskim poziomie). Kiedy korzystasz z DirectX, i tak jesteś już związany z Windows, więc utrata przenośności nie powinna być dużym problemem.[1] Tak, możesz ustawić szybkość harmonogramu na 1ms, ale jest to niezadowolone, ponieważ powoduje o wiele więcej przełączeń kontekstu i zużywa dużo więcej energii (w świecie, w którym coraz więcej urządzeń to urządzenia mobilne). To także nie jest rozwiązanie, ponieważ nadal nie sprawia, że sen jest bardziej niezawodny.
[2] Timer zwiększy priorytet wątku, co pozwoli mu przerwać kolejny wątek o równym priorytecie w połowie kwantowym i zostać zaplanowany jako pierwszy, co jest zachowaniem quasi-RT. To oczywiście nie jest prawda RT, ale jest bardzo blisko. Przebudzenie ze snu oznacza jedynie, że wątek będzie gotowy do zaplanowania w dowolnym momencie, kiedy tylko będzie to możliwe.
źródło
Nie jestem pewien, co chcesz osiągnąć, ograniczając liczbę klatek na sekundę aktualizacji i renderowania do 60. Jeśli ograniczysz je do tej samej wartości, możesz po prostu umieścić je w tym samym wątku.
Celem oddzielenia aktualizacji i renderowania w różnych wątkach jest to, aby oba były „prawie” niezależne od siebie, tak aby procesor graficzny mógł renderować 500 klatek na sekundę, a logika aktualizacji wciąż osiąga 60 klatek na sekundę. W ten sposób nie osiągasz bardzo wysokiego przyrostu wydajności.
Ale powiedziałeś, że chcesz wiedzieć, jak to działa, i jest w porządku. W C ++ muteks jest specjalnym obiektem służącym do blokowania dostępu do niektórych zasobów dla innych wątków. Innymi słowy, używasz muteksu, aby udostępnić sensowne dane tylko przez jeden wątek na raz. Aby to zrobić, jest to dość proste:
Źródło: http://en.cppreference.com/w/cpp/thread/mutex
EDYTOWAĆ : Upewnij się, że muteks ma klasę lub plik, tak jak w podanym linku, w przeciwnym razie każdy wątek utworzy swój własny muteks i nic nie osiągniesz.
Pierwszy wątek, który zablokuje muteks, będzie miał dostęp do kodu wewnątrz. Jeśli drugi wątek spróbuje wywołać funkcję lock (), będzie się blokował, dopóki pierwszy wątek go nie odblokuje. Muteks jest więc funkcją blokowania, w przeciwieństwie do pętli while. Blokowanie funkcji nie będzie obciążało procesora.
źródło
std::lock_guard
lub podobny, nie.lock()
/.unlock()
. RAII służy nie tylko do zarządzania pamięcią!