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:
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.
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.
źródło
Odpowiedzi:
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.
źródło
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.
źródło
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
ReaderWriterLockSlim
klasa 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ć
delegate
obiekt 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.źródło
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:
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:
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.źródło