Laboratorium 11: Programowanie równoległe i asynchroniczne #
Kod startowy #
- Examples/
- GameOfLife/
- .gitattributes
- .gitignore
- GameOfLife.UI/
- App.axaml
- App.axaml.cs
- Assets/
- Controls/
- Converters/
- Examples/
- GameOfLife.UI.csproj
- Models/
- Program.cs
- Services/
- ViewLocator.cs
- ViewModels/
- Views/
- app.manifest
- GameOfLife.sln
- task.en.md
- task.pl.md
Symulacja Gry w życie – Responsywne UI i Obliczenia Równoległe #
Celem zadania jest uzupełnienie fragmentów logiki aplikacji desktopowej, która symuluje automat komórkowy “Gry w życie”. Głównym założeniem naszej aplikacji nie jest sama grafika, lecz wydajność i responsywność aplikacji.
Opis elementów, z których składa się aplikacja desktopowa. Wszystko, o czym tutaj mowa, jest już zaimplementowane. Twoja “część” zaczyna się w Etap 1.
Aplikację można podzielić na 3 części:
- Lewy panel: Ten obszar służy do sterowania danymi wejściowymi.
- Przycisk
Choose directory...: Otwiera systemowe okno wyboru folderu. - Pasek postępu: Pokazuje postęp asynchronicznego skanowania plików.
- Lista plików: Wyświetla pliki znalezione w folderze. Wybór elementu z tej listy natychmiast przerywa obecną symulację i startuje nową dla wybranego pliku.
- Przycisk
- Obszar symulacji: Główna część ekranu, gdzie renderowana jest gra
- Dolny pasek statusu: Ten obszar służy do monitorowania wydajności i sterowania przebiegiem symulacji w czasie rzeczywistym.
- Status: Wyświetla komunikaty:
Choose directory...,Scanning directory...,Done.orazSimulation: <filename>. - Suwak Prędkości: Pozwala dynamicznie zmieniać opóźnienie pomiędzy generacjami bez restartowania symulacji.
- Status: Wyświetla komunikaty:
- Statystyki:
Generation: Numer aktualnego kroku symulacji.Living cells: Liczba żywych komórek (test poprawności algorytmu).Elapsed: Czas obliczenia ostatniej generacji w milisekundach.
Przydatne linki: #
- Microsoft Learn: Directory.EnumerateFiles Method
- Microsoft Learn: CancellationTokenSource.IsCancellationRequested Property
- Microsoft Learn: File.ReadAllLinesAsync Method
- Microsoft Learn: Task cancellation
- Microsoft Learn: Parallel.For Method
- Microsoft Learn: Interlocked.Add
- Microsoft Learn: Stopwatch Class
- Microsoft Learn: Progress
Class - Microsoft Learn: Task.Run Method
- Microsoft Learn: TaskCanceledException Class
Etap 1: Asynchroniczne wczytanie przykładów #
Aplikacja musi wczytywać pliki z planszami bez “zamrażania” interfejsu użytkownika.
Otwórz plik Services/FileService.cs.
- Metoda
EnumerateFilesAsync: (2 pkt.)- Zwróć ścieżki do plików tekstowych znajdujących się bezpośrednio w folderze
folderPathw postaciIAsyncEnumerable<string>. - Użyj
Directory.EnumerateFiles, aby pobrać listę plików.txt. - Obsłuż
CancellationToken– jeśli zażądano anulowania, przerwij pętlę. - Przed zwróceniem każdej ścieżki dodaj opóźnienie
100milisekund.
- Zwróć ścieżki do plików tekstowych znajdujących się bezpośrednio w folderze
- Metoda
LoadBoardAsync: (1 pkt.)- Wczytaj asynchronicznie wszystkie linie w pliku
filePath. - Zwróć wypełnioną tablicę dwywymiarową
bool[,]o rozmiarzerows × cols. - Znak
0w linijce zawartości pliku odpowiada żywej komórce (wartośćtruew tablicy). Znak nowej linii\nrozpoczyna nowy wiersz. - Każdy inny znak odpowiada martwej komórce (wartość `false w tablicy).
- Uwaga: Plik może zawierać mniej lub więcej linijek/znaków w linijce niż wynosi
rowslubcols. Należy wypełniać tablicę od lewego górnego rogu. Przyjmujemy, że cała reszta komórek jest martwa.
- Wczytaj asynchronicznie wszystkie linie w pliku
Przykładowa zawartość pliku wejściowego:
.....
...0.
.0.0.
..00.
.....Testowanie: #
Kliknij przycisk Choose directory... i wybierz folder z przykładami.
Lista po lewej stronie okna powinna się wypełnić zawartością wybranego folderu.
Etap 2: Silnik Gry i Równoległość #
Twoim zadaniem jest obliczenie stanu planszy w kolejnej generacji.
Gra toczy się na dwuwymiarowej planszy składającej się z komórek, które mogą być żywe (true) lub martwe (false).
Otwórz plik Models/LifeEngine.cs.
- Metoda
CalculateNextGeneration: (3 pkt.)- Zwróć obiekt
SimulationStepResult, który zawiera informację o całkowitej liczbie żywych komórek oraz czasie, w którym nowy stan został obliczony. - Stan komórki w następnej turze zależy od liczby jej żywych sąsiadów, którą zwraca gotowa funkcja
int CountLiveNeighbors(int y, int x). - Zastosuj zasady gry:
- Przeżycie: Żywa komórka z 2 lub 3 sąsiadami żyje dalej.
- Narodziny: Martwa komórka z dokładnie 3 sąsiadami ożywa.
- Śmierć: W każdym innym przypadku komórka staje się/pozostaje martwa.
- Użyj
Parallel.For, aby obliczać wiersze planszy równolegle. - Podpowiedź: Pamiętaj, że w trakcie obliczeń nie możesz modyfikować tablicy
Grid(stan obecny), z której czytasz dane. - Podpowiedź: Do sumowania całkowitej liczby żywych komórek w pętli równoległej użyj
Interlocked.Add. - Podpowiedź: Wykorzystaj pętlę
Parallel.FororazParallelOptionsw celu przekazania parametrutokendo pętli.
- Zwróć obiekt
Etap 3: Połączenie UI z logiką #
To najważniejszy etap, w którym połączysz UI z logiką, dbając o to, by aplikacja nie “wisiała” i reagowała na zmiany.
Otwórz plik ViewModels/MainWindowViewModel.cs. Znajduje się w nim część definicji klasy (partial class), którą będziesz rozwijać.
Pozostała część (mocno związana z wykorzystanym frameworkiem graficznym Avalonia) znajduje się w ViewModels/UI/MainWindowViewModel.UI.cs (jej nie musisz edytować).
Wszystkie zmienne zapisane specjalną czcionką są albo parametrami funkcji albo istnieją w pliku ViewModels/UI/MainWindowViewModel.UI.cs i nie musisz ich deklarować.
- Metoda
SimulationLoop: (2 pkt.)- W pętli oblicza nowy stan gry.
- W każdej iteracji:
- Raportuje progres przy pomocy argumentu
progress. Obiekt typuSimulationFramepowinien zawierać sklonowaną (.Clone()) wewnętrzną tablicę silnika_engine. - Opóźnia swoje wykonanie o
SimulationDelaymilisekund.
- Raportuje progres przy pomocy argumentu
- Kończy działanie w zależności od stanu parametru
token.
- Metoda
StartSimulationFromFileAsync: (4 pkt.)- Przerywa wykonanie poprzedniej symulacji (użyj
_simulationCancellationTokenSource). - Jeżeli poprzednia symulacja była w toku, to
await-uje_currentSimulationTask. - Ładuje zawartość planszy z pliku
filePathprzy pomocy_fileService. Jej rozmiar toBoardSize × BoardSize. - Ustawia stan silnika gry
_engine(przy pomocy metodyLoadStatew klasieLifeEngine) oraz początkowe wartości dla paska statusu (statystyki:Generation,StatusText). - Uruchamia metodę
SimulationLoopbez oczekiwania (bezawait) przy pomocyTask.Run. Obiekt typuTaskzwrócony przez metodę należy przypisać do_currentSimulationTask. - Definiuje logikę raportowania progresu (tworzy obiekt typu
Progress<SimulationFrame>). - Progres aktualizuje:
- Liczbę żywych komórek
LiveCells. - Ostatni czas obliczeń
LastCalculationTimeMs. - Generację
Generation - Odświeża planszę poprzez przypisanie nowego stanu planszy do zmiennej
CurrentGrid.
- Liczbę żywych komórek
- Przerywa wykonanie poprzedniej symulacji (użyj
Testowanie całości: #
- Kliknij
Choose directory...– pliki powinny pojawiać się na liście pojedynczo (dziękiTask.Delayw serwisie). - Kliknij plik na liście – symulacja powinna ruszyć.
- Zmień suwak prędkości – symulacja powinna przyspieszyć/zwolnić natychmiast.
Przykładowe rozwiązanie #
- GameOfLife/
- .gitattributes
- .gitignore
- GameOfLife.UI/
- App.axaml
- App.axaml.cs
- Assets/
- Controls/
- Converters/
- Examples/
- GameOfLife.UI.csproj
- Models/
- Program.cs
- Services/
- ViewLocator.cs
- ViewModels/
- Views/
- app.manifest
- GameOfLife.sln