Jak sprawić, by przekazywanie wiadomości między wątkami w silniku wielowątkowym było mniej uciążliwe?

18

Silnik C ++, nad którym obecnie pracuję, jest podzielony na kilka dużych wątków - Generacja (do tworzenia treści proceduralnych), Rozgrywka (do sztucznej inteligencji, skryptów, symulacji), Fizyka i Rendering.

Wątki komunikują się ze sobą za pośrednictwem małych obiektów wiadomości, które przechodzą od wątku do wątku. Przed krokiem wątek przetwarza wszystkie przychodzące wiadomości - aktualizacje przekształceń, dodawanie i usuwanie obiektów itp. Czasami jeden wątek (Generacja) utworzy coś (Sztuka) i przekaże go do innego wątku (Rendering) w celu stałego posiadania.

Na początku procesu zauważyłem kilka rzeczy:

  1. System przesyłania wiadomości jest uciążliwy. Utworzenie nowego typu wiadomości oznacza podklasę podstawowej klasy Message, utworzenie nowego wyliczenia dla tego typu i zapisanie logiki tego, jak wątki powinny interpretować nowy typ wiadomości. Jest to próg zwalniający w rozwoju i podatny na błędy w literówkach. (Sidenote - praca nad tym sprawia, że ​​doceniam, jak świetne mogą być dynamiczne języki!)

    Czy jest na to lepszy sposób? Czy powinienem użyć czegoś takiego jak boost :: bind, aby zrobić to automatycznie? Martwię się, że jeśli to zrobię, stracę możliwość powiedzenia, posortowania wiadomości według typu lub czegoś takiego. Nie jestem pewien, czy tego rodzaju zarządzanie w ogóle stanie się konieczne.

  2. Pierwszy punkt jest ważny, ponieważ wątki te bardzo się komunikują. Tworzenie i przekazywanie wiadomości to duża część spraw, aby coś się działo. Chciałbym usprawnić ten system, ale także być otwarty na inne paradygmaty, które mogą być równie pomocne. Czy są różne projekty wielowątkowe, o których warto pomyśleć, aby ułatwić to?

    Na przykład niektóre zasoby są rzadko zapisywane, ale często czytane z wielu wątków. Czy powinienem być otwarty na pomysł posiadania wspólnych danych, chronionych przez muteksy, do których dostęp mają wszystkie wątki?

To mój pierwszy projekt od podstaw z myślą o wielowątkowości. Na tym wczesnym etapie wydaje mi się, że idzie naprawdę dobrze (biorąc pod uwagę), ale martwię się skalowaniem i własną wydajnością we wdrażaniu nowych rzeczy.

Raptormeat
źródło
6
Naprawdę nie ma tutaj jednego, ukierunkowanego pytania, i dlatego ten post nie pasuje do stylu pytań i odpowiedzi na tej stronie. Zalecam podzielenie swojego posta na osobne posty, po jednym na pytanie, i ponowne skoncentrowanie pytań, tak aby zadawali pytania o konkretny problem, który faktycznie masz, zamiast niejasnego zbioru wskazówek lub porad.
2
Jeśli chcesz wziąć udział w bardziej ogólnej rozmowie, polecam wypróbowanie tego postu na forach na gamedev.net . Jak powiedział Josh, ponieważ twoje „pytanie” nie jest pojedynczym konkretnym pytaniem, bardzo trudno byłoby go dostosować w formacie StackExchange.
Cypher,
Dzięki za opinie! Miałem nadzieję, że ktoś z większą wiedzą może mieć jeden zasób / doświadczenie / paradygmat, który może rozwiązać kilka moich problemów naraz. Mam wrażenie, że jeden wielki pomysł może zsyntetyzować moje różne problemy w jedną rzecz, za którą tęsknię, i myślałem, że ktoś z większym doświadczeniem, niż mogłem, może to rozpoznać ... Ale może nie, i tak czy inaczej - wzięte pod uwagę !
Raptormeat
Zmieniłem nazwę twojego tytułu, aby był bardziej szczegółowy w przekazywaniu wiadomości, ponieważ pytania typu „porady” sugerują, że nie ma konkretnego problemu do rozwiązania (a zatem w dzisiejszych czasach zamknęłbym jako „nie prawdziwe pytanie”).
Tetrad
Czy na pewno potrzebujesz osobnych wątków do fizyki i rozgrywki? Te dwie wydają się być bardzo powiązane. Ponadto trudno jest wiedzieć, jak oferować porady, nie wiedząc, jak każdy z nich się komunikuje i z kim.
Nicol Bolas,

Odpowiedzi:

10

Jeśli chodzi o twój szerszy problem, zastanów się, jak najprościej znaleźć sposoby na ograniczenie komunikacji między wątkami. Jeśli to możliwe, lepiej całkowicie uniknąć problemów z synchronizacją. Można to osiągnąć poprzez podwójne buforowanie danych, wprowadzając opóźnienie pojedynczej aktualizacji, ale znacznie ułatwiając pracę z udostępnionymi danymi.

Nawiasem mówiąc, czy zastanawiałeś się nie nad wątkami według podsystemu, a zamiast tego za pomocą spawnowania wątków lub pul wątków do rozwidlenia według zadania? (zobacz to w odniesieniu do konkretnego problemu, w przypadku łączenia wątków.) Ten krótki artykuł zwięźle opisuje cel i zastosowanie wzorca puli. Zobacz te pouczające odpowiedzirównież. Jak wspomniano, pule wątków zwiększają skalowalność jako bonus. I jest to „pisz raz, używaj gdziekolwiek”, w przeciwieństwie do konieczności zdobywania wątków opartych na podsystemie, aby grały przyjemnie za każdym razem, gdy piszesz nową grę lub silnik. Istnieje również wiele solidnych zewnętrznych rozwiązań w zakresie łączenia wątków. Łatwiej byłoby zacząć od odradzania wątków, a później przejść do pul wątków, jeśli trzeba ograniczyć nakłady na odradzanie i niszczenie wątków.

Inżynier
źródło
1
Jakieś rekomendacje dla określonych bibliotek pul wątków do sprawdzenia?
imre
Nick - wielkie dzięki za odpowiedź. Co do twojego pierwszego punktu - myślę, że to świetny pomysł i prawdopodobnie kierunek, w którym się wprowadzę. W tej chwili jest na tyle wcześnie, że nie wiem jeszcze, co trzeba by podwójnie buforować. Będę o tym pamiętać, ponieważ z czasem to się utrwala. Do drugiego punktu - dziękuję za sugestię! Tak, zalety zadań wątkowych są oczywiste. Przeczytam twoje linki i pomyślę o tym. Nie jestem w 100% pewien, czy to zadziała dla mnie / jak sprawić, by działało dla mnie, ale na pewno poważnie się zastanowię. Dzięki!
Raptormeat
1
@imre zajrzyj do biblioteki Boost - mają kontrakty, które są przyjemnym / łatwym sposobem na zbliżenie się do tych rzeczy.
Jonathan Dickinson
5

Zapytałeś o różne wielowątkowe projekty. Mój przyjaciel powiedział mi o tej metodzie, która moim zdaniem była całkiem fajna.

Chodzi o to, że będą 2 kopie każdego elementu gry (marnotrawstwo, wiem). Jedna kopia byłaby kopią obecną, a druga kopią przeszłą. Obecna kopia jest przeznaczona wyłącznie do zapisu , a poprzednia kopia jest przeznaczona tylko do odczytu . Po przejściu do aktualizacji przypisujesz zakresy listy encji do tylu wątków, ile uznasz za stosowne. Każdy wątek ma dostęp do zapisu do obecnych kopii w przypisanym zakresie, a każdy wątek ma dostęp do odczytu do wszystkich poprzednich kopii bytów, a zatem może aktualizować przypisane obecne kopie przy użyciu danych z poprzednich kopii bez blokowania. Pomiędzy każdą klatką obecna kopia staje się kopią przeszłą, jednak chcesz poradzić sobie z zamianą ról.

John McDonald
źródło
4

Mieliśmy ten sam problem, tylko z C #. Po długim i trudnym zastanowieniu się nad łatwością (lub jej brakiem) tworzenia nowych wiadomości, najlepszym, co mogliśmy zrobić, było stworzenie dla nich generatora kodu. Jest to trochę brzydkie, ale użyteczne: podając tylko opis treści wiadomości, generuje klasę wiadomości, wyliczenia, kod obsługi symboli zastępczych itp. - cały ten kod jest prawie taki sam za każdym razem i naprawdę podatny na literówki.

Nie jestem do końca zadowolony, ale lepiej jest pisać cały ten kod ręcznie.

Jeśli chodzi o udostępniane dane, najlepszą odpowiedzią jest oczywiście „to zależy”. Ale ogólnie, jeśli niektóre dane są często odczytywane i potrzebne wielu wątkom, dzielenie się nimi jest tego warte. Jeśli chodzi o bezpieczeństwo wątków, najlepszym rozwiązaniem jest uczynienie go niezmiennym , ale jeśli nie jest to możliwe, mutex może. W języku C # istnieje ReaderWriterLockSlimklasa zaprojektowana specjalnie dla takich przypadków; Jestem pewien, że istnieje odpowiednik C ++.

Innym pomysłem na komunikację wątków, który prawdopodobnie rozwiązuje pierwszy problem, jest przekazywanie programów obsługi zamiast komunikatów. Nie jestem pewien, jak to rozwiązać w C ++, ale w C # możesz wysłać delegateobiekt do innego wątku (jak w, dodać go do jakiejś kolejki komunikatów) i faktycznie wywołać tego delegata z wątku odbierającego. Umożliwia to tworzenie wiadomości „ad hoc” na miejscu. Bawiłem się tylko tym pomysłem, nigdy nie wypróbowałem go w produkcji, więc może okazać się zły.

Nieważne
źródło
Dzięki za wszystkie świetne informacje! Ostatni kawałek o modułach obsługi jest podobny do tego, o którym wspominałem o używaniu wiązania lub funktorów do przekazywania funkcji. Podoba mi się ten pomysł - mógłbym go wypróbować i sprawdzić, czy jest do bani, czy jest niesamowity: D Może zacznę od stworzenia klasy CallDelegateMessage i zanurzenia palca u nogi w wodzie.
Raptormeat
1

Jestem tylko w fazie projektowania wątkowego kodu gry, więc mogę dzielić się tylko swoimi przemyśleniami, a nie faktycznymi doświadczeniami. Powiedziawszy to, myślę w następujący sposób:

  • Większość danych gry należy udostępnić, aby uzyskać dostęp tylko do odczytu .
  • Zapisywanie danych jest możliwe przy użyciu pewnego rodzaju wiadomości.
  • Aby uniknąć aktualizacji danych podczas czytania innego wątku, pętla gry ma dwie odrębne fazy: odczyt i aktualizację.
  • W fazie odczytu:
  • Wszystkie udostępnione dane są tylko do odczytu dla wszystkich wątków.
  • Wątki mogą obliczać elementy (przy użyciu lokalnego magazynu wątków) i generować żądania aktualizacji , które są w zasadzie obiektami poleceń / komunikatów, umieszczonymi w kolejce, do zastosowania później.
  • W fazie aktualizacji:
  • Wszystkie udostępnione dane są tylko do zapisu. Dane należy przyjmować w stanie nieznanym / niestabilnym.
  • To tutaj przetwarzane są obiekty żądania aktualizacji.

Myślę, że (choć nie jestem pewien) teoretycznie powinno to oznaczać, że zarówno w fazie odczytu, jak i aktualizacji, dowolna liczba wątków może działać jednocześnie przy minimalnej synchronizacji. W fazie odczytu nikt nie zapisuje udostępnionych danych, więc nie powinny wystąpić problemy z współbieżnością. Faza aktualizacji jest trudniejsza. Problemem mogą być równoległe aktualizacje tego samego kawałka danych, więc tutaj jest pewna synchronizacja. Jednak nadal mogę uruchomić dowolną liczbę wątków aktualizacji, o ile działają one na różnych zestawach danych.

Podsumowując, myślę, że takie podejście dobrze nadaje się do systemu puli wątków. Problematyczne części to:

  • Synchronizowanie wątków aktualizacji (upewnij się, że żaden wątek nie próbuje zaktualizować tego samego zestawu danych).
  • Upewniając się, że w fazie odczytu żaden wątek nie może przypadkowo zapisać udostępnionych danych. Obawiam się, że byłoby zbyt wiele miejsca na błędy programistyczne i nie jestem pewien, jak wiele z nich można łatwo złapać za pomocą narzędzi do debugowania.
  • Pisanie kodu w taki sposób, że nie można polegać na tym, że wyniki pośrednie są od razu dostępne do czytania. Oznacza to, że nie możesz pisać, x += 2; if (x > 5) ...jeśli x jest współdzielony. Musisz utworzyć lokalną kopię X lub wygenerować żądanie aktualizacji i wykonać warunek tylko w następnym uruchomieniu. To ostatnie oznaczałoby wiele dodatkowych kodów zachowujących stan wątku lokalnego.
imre
źródło