Cykliczny collector garbage (GC) Pythona narzuca ścisłe ograniczenie sekwencyjności podczas destrukcji cyklicznych grafów obiektów zawierających finalizatory. Gdy GC wykrywa niedostępne cykle, najpierw segreguje obiekty posiadające metody __del__ od tych, które ich nie mają. W przypadku obiektów z finalizatorami, GC wyraźnie czyści wszystkie słabe referencje (wywołując ich wywołania zwrotne z None jako argumentem) przed wywołaniem metod __del__. Taka kolejność zapobiega ponownemu ożywieniu, co jest niebezpiecznym stanem, w którym umierający obiekt staje się ponownie dostępny, ponieważ wywołanie zwrotne lub finalizator tworzy nową silną referencję do niego. Poprzez unieważnienie słabych referencji przed wykonaniem finalizatora, Python gwarantuje, że obiekt pozostaje niedostępny przez cały proces destrukcji, co zapewnia deterministyczne zbieranie śmieci.
Na platformie do handlu częstotliwościowego zbudowanej w Pythonie, zaimplementowaliśmy własną pula obiektów do zarządzania pakietami danych rynkowych. Każdy obiekt pakietu rejestrował wywołanie zwrotne słabej referencji, aby rejestrować metryki opóźnienia, gdy pakiet był zbierany jako śmieci. Dodatkowo, pakiety przechowywały otwarte zasoby gniazd sieciowych zarządzane za pomocą metod __del__, aby zapewnić automatyczne zamykanie połączeń. Podczas testów obciążeniowych aplikacja wykazywała poważne wycieki pamięci, w których obiekty pakietów utrzymywały się w pamięci na zawsze pomimo tego, że logicznie były niedostępne.
Rozwiązanie 1: Polegać na automatycznym zbieraniu śmieci bez interwencji.
Początkowa architektura zakładała, że GC CPythona automatycznie poradzi sobie z cyklicznymi referencjami między pakietami a ich wewnętrznymi rejestrami wywołań. Jednak podejście to nie udało się, ponieważ interakcja między metodami __del__ a wywołaniami zwrotnymi weakref w obiektach cyklicznych wywołała ponowne ożywienie. Wywołania zwrotne słabej referencji były uruchamiane podczas zbierania i przypadkowo ponownie rejestrowały obiekty pakietów w globalnym słowniku metryk zanim collector garbage mógł całkowicie przerwać cykle. To stworzyło zombie obiekty, które zużywały pamięć, ale były częściowo zniszczone, prowadząc do niespójnych stanów gniazd i wyczerpania deskryptorów plików.
Rozwiązanie 2: Wprowadzić jawne metody release() i ręczne czyszczenie.
Rozważaliśmy całkowite usunięcie __del__ i wymaganie od programistów, aby jawnie wywoływali packet.release() przed dereferencją. Chociaż wyeliminowało to problemy z interakcją z GC, wprowadziło znaczną kruchość API. Programiści często zapominali zwolnić pakiety w ścieżkach obsługi wyjątków, a powstałe wycieki zasobów były trudniejsze do debugowania niż pierwotne problemy z pamięcią. Ponadto, podejście jawne wymagało rozległych bloków try-finally w całym kodzie przetwarzania asynchronicznego, zagracając logikę biznesową obawami o zarządzanie pamięcią i obniżając ogólną czytelność kodu.
Rozwiązanie 3: Refaktoryzacja z użyciem weakref.finalize i menedżerów kontekstu.
Wybrane rozwiązanie zastąpiło metody __del__ rejestracjami weakref.finalize i menedżerami kontekstu (instrukcje with). Usunęliśmy wszystkie metody __del__ z obiektów pakietów, zapewniając, że GC może traktować je jako standardowe cykliczne odpady bez ograniczeń porządkowania finalizacji. W celu powiadamiania o czyszczeniu, przeszliśmy z wywołań zwrotnych weakref.ref na weakref.finalize, który nie przekazuje obiektu do funkcji wywołania zwrotnego, zapobiegając tym samym ponownemu ożywieniu. Gniazda sieciowe były zarządzane przez jawne menedżery kontekstu, które gwarantowały zamknięcie bez względu na wyjątki.
To podejście odniosło sukces, ponieważ było zgodne z architekturą zbierania śmieci Pythona. Poprzez wyeliminowanie finalizatorów z obiektów cyklicznych, pozwoliliśmy GC na bezpieczne czyszczenie słabych referencji i zbieranie cykli bez ryzyka ponownego ożywienia. Zużycie pamięci ustabilizowało się, a metryki opóźnienia nadal były poprawnie rejestrowane bez zakłócania cykli życia obiektów.
import weakref import gc class DataPacket: def __init__(self, packet_id): self.packet_id = packet_id self.peer = None # Tworzy cykle w produkcji # Usunięto __del__, aby uniknąć problemów z kolejnością GC def log_cleanup(ref, pid): # Bezpieczne: odbiera packet_id, a nie obiekt print(f"Pakiet {pid} został usunięty") # Użycie pakiet = DataPacket(123) paket.peer = pakiet # Cykl samoreferencyjny # Bezpieczna finalizacja bez ryzyka ponownego ożywienia weakref.finalize(paket, log_cleanup, pakiet.packet_id) paket = None gc.collect() # Bezpiecznie zbiera bez ponownego ożywienia
Dlaczego wywołanie gc.collect() nie gwarantuje natychmiastowego wywołania zwrotów słabych referencji dla wszystkich obiektów?
Kandydaci często zakładają, że gc.collect() synchronizuje się z wywołaniem wszystkich wywołań zwrotnych weakref. Jednak wywołania zwrotne weakref są uruchamiane tylko dla obiektów, które stają się niedostępne podczas konkretnej cyklu zbierania. Jeśli obiekt wciąż jest osiągalny z korzeni, jego wywołania pozostają uśpione. Ponadto, CPython przetwarza cykliczne odpady w fazach: obiekty z metodami __del__ są obsługiwane oddzielnie, a ich słabe referencje są czyszczone przed wywołaniem finalizatorów. Wywołania zwrotne dla tych obiektów mogą być opóźnione lub przetwarzane w określonej kolejności w odniesieniu do generacji będącej zbieraną. Zrozumienie, że wywołania zwrotne weakref są powiązane z wydarzeniami destrukcji obiektów, a nie z wyraźnym wywołaniem gc.collect(), jest kluczowe dla przewidywania zachowań czyszczenia.
Czym jest niebezpieczeństwo "ponownego ożywienia" w cyklicznym zbieraniu śmieci Pythona?
Ponowne ożywienie występuje, gdy metoda __del__ obiektu lub wywołanie zwrotne weakref tworzy nową silną referencję do obiektu będącego w trakcie destrukcji, sprawiając, że staje się on znowu osiągalny w trakcie zbierania. Jest to niebezpieczne, ponieważ GC już rozpoczął finalizację wewnętrznego stanu obiektu, co może pozostawić go w niespójnym stanie. Python zapobiega ponownemu ożywieniu przez czyszczenie słabych referencji przed wywołaniem finalizatorów. Gdy GC wykrywa cykliczne odpady, identyfikuje obiekty z __del__, przenosi je do tymczasowej listy, czyści wszystkie wpisy weakref (wywołując wywołania zwrotne z None) i tylko wtedy wykonuje finalizatory. To zapewnia, że w momencie, gdy kod użytkownika działa, obiekt jest definitywnie niedostępny przez słabe referencje.
Jak weakref.finalize różni się od standardowych wywołań zwrotnych weakref.ref pod względem bezpieczeństwa zbierania śmieci?
weakref.finalize jest zaprojektowany w sposób, aby uniknąć problemu ponownego ożywienia. W przeciwieństwie do weakref.ref, który przekazuje umierający obiekt jako argument do wywołania zwrotnego (tworzącego tymczasową silną referencję, która mogłaby zostać przechowana), finalize otrzymuje obiekt, ale nie przekazuje go do zarejestrowanej funkcji wywołania zwrotnego. Zamiast tego wywołuje funkcję zwrotną z wcześniej zarejestrowanymi argumentami, które nie mogą obejmować samego obiektu. Ten projekt zapewnia, że wywołanie zwrotne nie może ponownie ożywić obiektu, ponieważ nigdy nie otrzymuje żywej referencji do niego. Kandydaci często przeoczają, że obiekty finalize są utrzymywane w życiu przez wewnętrzny rejestr Pythona aż do wywołania funkcji zwrotnej, co zapewnia, że czyszczenie odbywa się nawet jeśli oryginalny obszar tworzenia został zakończony.