Na wstępnie od razu chciałbym dodać, że dla większości aplikacji, dalsze rozważania w tym poście nie mają sensu i należy to traktować jako ciekawostkę. Dla części systemów może to jednak być ważne a mianowicie dla oprogramowania działającego w “czasie rzeczywistym”, wymagającego low-latency.
Dzisiejszy wpis może powstał trochę zbyt wcześnie ale wkrótce powinna pojawić się druga część mojego artykułu o GC, który będzie dotyczył różnych trybów kolekcji. Jednym z tych trybów jest wykonywanie kolekcji w sposób nierównoległy. W skrócie, znajdywanie wszystkich nieosiągalnych obiektów ma miejsce na wątku głównym (główny to ten, który powoduje kolekcje). W podejściach równoległych, istnieje osobny wątek, który na bieżąco sprawdza czy jakieś obiekty są już nieosiągalne. Podejście równoległe daje wrażenie płynności aplikacji (pauzy na kolekcje są dużo krótsze) ale wymaga to więcej zasobów i jest to na końcu po prostu trochę wolniejsze (synchronizacja itp.). Najszybsze jest po prostu zablokowanie wątków i przejście przez drzewo obiektów. Niestety powoduje to opóźnienia i w niektórych aplikacjach jest to bardzo niekorzystne – wyobraźmy sobie aplikację, wyświetlającą animację, która musi się co jakiś czas zatrzymać aby wykonać kolekcję.
Istnieje wiele rozwiązań na ten problem. Cześć z nich pokażę w nadchodzącym artykule. W dzisiejszym wpisie chciałbym zaprezentować jednak trochę inny mechanizm. Wyobraźmy sobie, że mamy rozproszony system przetwarzający tysiące zapytań. Dodajmy również kolejne wymaganie a mianowicie “low latency” – nie możemy sobie pozwolić, że jakieś zapytanie będzie opóźnione z powodu GC. W poście chciałbym pokazać rozwiązanie polegające na transferze zapytań do np. innego węzła w momencie, gdy zbliża się czas kolekcji. Jeśli zatem wiemy, że za 5 sekund będziemy mieli pełną kolekcję wtedy przekierowujemy ruch do innego węzła.
W .NET istnieją 4 funkcję, które będą nam szczególnie przydatne:
- GC.RegisterForFullGCNotification()
- GC.CancelFullGCNotification()
- GC.WaitForFullGCApproach()
- GC.WaitForFullGCComplete()
Utworzymy więc osobny wątek, który będzie monitorował stan GC. W momencie, gdy zbliża się kolekcja, wyśle on komunikat, że system będzie przez jakiś czas obciążony. W celu otrzymania powiadomień od GC należy wywołać funkcję GC.RegisterForFullGCNotification:
GC.RegisterForFullGCNotification(maxGenerationThreshold: 10, largeObjectHeapThreshold: 10);
Funkcja jest bardzo prosta ale niestety parametry wejściowe to trochę czarna magia. W dokumentacji możemy znaleźć następujący opis:
maxGenerationThreshold
- Type: System.Int32
A number between 1 and 99 that specifies when the notification should be raised based on the objects surviving in generation 2.
- largeObjectHeapThreshold
- Type: System.Int32
A number between 1 and 99 that specifies when the notification should be raised based on objects allocated in the large object heap.
Niestety nie jest to zbyt precyzyjne a nawet powiedziałbym bardzo mylące! Podane progi to nie wartości absolutne. Jeśli przekażemy 10 i 10 nie znaczy to, że dostaniemy powiadomienie gdy 10 obiektów zostało wypromowanych do GEN2. Należy te parametry traktować jako wartości relatywne i:
- Podanie zbyt wysokiej liczby spowoduje, że dostaniemy powiadomienie bardzo szybko, możliwe, że długo przed faktyczną kolekcją.
- Podanie zbyt małej wartości może nieść ryzyko, że powiadomienia po prostu nie zostaną wysłane.
Zatem należy na nie patrzyć relatywnie – 90 wyśle powiadomienie dużo szybciej niż np. 5.
Mając mechanizm powiadomień możemy stworzyć nowy wątek, który będzie sprawdzał stan pamięci:
private static void MonitorMemory() { while (true) { GCNotificationStatus gcStartedStatus = GC.WaitForFullGCApproach(-1); if (gcStartedStatus == GCNotificationStatus.Succeeded) Console.WriteLine("Nadchodzi kolekcja.Pamiec:{0}",GC.GetTotalMemory(false)); else Console.WriteLine("Cos poszlo nie tak..."); GCNotificationStatus gcCompletedStatus = GC.WaitForFullGCComplete(-1); if (gcCompletedStatus == GCNotificationStatus.Succeeded) Console.WriteLine("Kolekcja zakonczona.Pamiec:{0}", GC.GetTotalMemory(false)); else return; } }
Należy mieć na uwadze, że rozwiązanie dotyczy wyłącznie nierównoległego GC więc należy takowy ustawić w pliku konfiguracyjnym (więcej o concurrent GC w nadchodzącym artykule):
<configuration> <runtime> <gcConcurrent enabled="false"/> </runtime> </configuration>
W głównym wątku możemy stworzyć pętle, która sztucznie alokuje pamięć, tak abyśmy mogli przetestować metodę MonitorMemory:
class Program { static void Main(string[] args) { GC.RegisterForFullGCNotification(maxGenerationThreshold: 10, largeObjectHeapThreshold: 10); Task.Factory.StartNew(MonitorMemory); while(true) { byte[]buffer=new byte[85000]; Thread.Sleep(100); } } private static void MonitorMemory() { while (true) { GCNotificationStatus gcStartedStatus = GC.WaitForFullGCApproach(-1); if (gcStartedStatus == GCNotificationStatus.Succeeded) Console.WriteLine("Nadchodzi kolekcja.Pamiec:{0}",GC.GetTotalMemory(false)); else Console.WriteLine("Cos poszlo nie tak..."); GCNotificationStatus gcCompletedStatus = GC.WaitForFullGCComplete(-1); if (gcCompletedStatus == GCNotificationStatus.Succeeded) Console.WriteLine("Kolekcja zakonczona.Pamiec:{0}", GC.GetTotalMemory(false)); else return; } } }
Na zakończenie kilka uwag. Metody nie gwarantują, że powiadomienie zostanie wysłane zatem nie można projektować algorytmów w ten sposób, że zawsze spodziewają się tego. W celu wstrzymania powiadomień wystarczy wywołać CancelFullGCNotification.
Jaki to ma sens w praktyce? Dla większości aplikacji: żaden. Dla systemów low-latency – nie wiem, nie wykorzystałem jeszcze tego w praktyce i nie wiem czy jest sens. Stworzenie wątku to w końcu skomplikowana sprawa i wiąże się z tym pewne zużycie pamięci.