Automatická správa paměti v C

C a C++ Céčko Pokročilé konstrukce Automatická správa paměti v C

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

Přestože je C nízkoúrovňový jazyk bez moderních vymožeností, je možné (a žádoucí) spravovat v něm paměť automaticky. V tomto článku si představíme osvědčený způsob správy paměti v C založený na počítání referencí.

C není objektově orientované, nemá tedy dědičnost ani polymorfismus. Zatímco dědičnost není nezbytně nutná (viz např. Go), polymorfismu se v některých případech nelze vyhnout. Naše malá knihovna pro práci s objekty v C (COX=C Object Extensions) si tedy musí pro každý typ objektu ("třídu") držet v paměti adresu na funkce sdílené více objekty, pokud se pro různé typy objektů chovají jinak. V našem jednoduchém případě budeme mít jen funkci (resp. metodu) pro uvolnění prostředků objektu a převod na textovou reprezentaci (pro ladění):

struct cox_type {
        void(*finalizer)(void*);
        cox_string_t(*descriptor)(void*);
};

Každý objekt bude mít hlavičku obsahující odkaz na typ a čítač referencí:

struct cox_base {
        struct cox_type* type;
        unsigned int refcount;
};

Chceme-li například automaticky spravovaný objekt pro řetězce, deklarujeme jej takto:

struct cox_string {
        struct cox_base base;
        char* cstr;
        unsigned long len;
};
typedef struct cox_string* cox_string_t;

Každá instance tedy kromě vlastního řetězce (cstr) a jeho délky (len) obsahuje i informace o typu a čítač referencí. Typ je definován takto:

static struct cox_type cox_string_type = {.finalizer = &cox_string_destroy, .descriptor = &cox_string_describe };

Uvolnění řetězce je jednoduché, nejdříve vrátíme paměť alokovanou pro cstr a poté objekt samotný. Popis objektu je ještě jednodušší, řetězec totiž v tomto případě vrací sebe sama:

static void cox_string_destroy(void* obj) {
        cox_string_t str = obj;
        free(str->cstr);
        free(str);
#ifdef DEBUG
        printf("cox_string destroyed\n");
#endif
}

static cox_string_t cox_string_describe(void* obj) {
        return obj;
}

Nepříliš složité je také vytvoření objektu řetězce:

cox_string_t cox_string_create(const char* s) {
        cox_string_t str = malloc(sizeof(struct cox_string));
        str->base.type = &cox_string_type;
        str->base.refcount = 1;
        str->len = strlen(s);
        str->cstr = malloc(str->len + 1);
        strcpy(str->cstr, s);
        return str;
}

Stačí alokovat paměť, přiřadit informaci o typu, čítač referencí nastavit na 1 a nakonec alokovat paměť pro cstr a text zkopírovat.

Pokud již objekt nepoužíváme, uvolníme jej. Pro libovolný objekt implementovaný pomocí naší knihovny budeme pro uvolnění používat funkci cox_release:

void cox_release(void* obj) {
        struct cox_base* base = obj;
        cox_refcount_lock();
        base->refcount--;
        int should_destroy = base->refcount == 0;
        cox_refcount_unlock();
        if (should_destroy) {
                base->type->finalizer(obj);
        }
}

Tato funkce sníží čítač referencí a pokud tento klesne na nulu, tj. na objekt neexistují žádné další reference, zavolá se destruktor z informace o typu, čímž se uvolní veškeré prostředky používané objektem (včetně paměti pro objekt samotný).

Všimněte si funkcí cox_refcount_lock() a cox_refcount_unlock(). Ty jsou důležité při aktualizaci čítače referencí ve vícevláknovém prostředí. Jejich implementace závisí na konkrétním operačním systému, nejjednodušší je použít mutex nebo semafor (např. ve Windows funkci CreateSemaphore atd.).

Pokud tedy vytvoříme řetězec a po použití na něj zavoláme cox_release(), objekt se automaticky uvolní:

cox_string_t str = cox_string_create("Hello, world!");
...
cox_release(str);

Pokud takový objekt přidáme např. do nějaké kolekce, tato kolekce zvýší jeho čítač referencí a cox_release objekt neodstraní z paměti, protože ještě existuje další reference.

Abychom nemuseli explicitně volat cox_release() ve všech větvích kódu, deklarujeme si atribut __auto:

#define __auto __attribute__((cleanup(cox_release_indirect)))
void cox_release_indirect(void* p) {
        cox_release(*(void**)p);
}

Potom můžeme deklarovat proměnné takto:

__auto cox_string_t str = cox_string_create("Hello, world!");

díky čemuž není nutné použití cox_release(), protože taková proměnná se automaticky uvolní (resp. sníží se její čítač referencí) po opuštění jejího rozsahu platnosti.

Atribut __auto je užitečný pro lokální proměnné, problém ale nastává, chceme-li vrátit objekt z funkce, aniž bychom se museli starat o jeho uvolnění, neboť návratem z funkce opouštíme rozsah platnosti proměnných. Pro tento účel se hodí tzv. autorelease pool:

struct cox_autoreleasepool {
        void** objs;
        unsigned int count;
        unsigned int capacity;
        struct cox_autoreleasepool* next;
        struct cox_autoreleasepool* prev;
};
typedef struct cox_autoreleasepool* cox_autoreleasepool_t;

Každé vlákno má svůj zásobník poolů, do kterých ukládá objekty určené k pozdějšímu (automatickému) uvolnění. Typicky tedy budeme mít:

cox_string_t str = cox_string_create("Hello, world!");
cox_autorelease(str);
...
cox_autoreleasepool_destroy(pool);

přičemž pool za nás obvykle spravuje smyčka událostí nebo nějaký podobný mechanismus, takže se o explicitní správu poolů starat nemusíme. Příslušná mašinerie je definována takto:

__thread static cox_autoreleasepool_t autopool = NULL;

cox_autoreleasepool_t cox_autoreleasepool_create() {
        cox_autoreleasepool_t pool = malloc(sizeof(struct cox_autoreleasepool));
        pool->count = 0;
        pool->capacity = 100;
        pool->objs = malloc(sizeof(void*) * pool->capacity);
        pool->next = NULL;
        if (autopool != NULL) autopool->next = pool;
        pool->prev = autopool;
        autopool = pool;
        return pool;
}

void cox_autoreleasepool_destroy(cox_autoreleasepool_t pool) {
        if (pool->next != NULL) cox_autoreleasepool_destroy(pool->next);
        for (int i = 0; i < pool->count; i++) cox_release(pool->objs[i]);
        free(pool->objs);
        autopool = pool->prev;
        free(pool);
#ifdef DEBUG
        printf("cox_autoreleasepool destroyed\n");
#endif
}

void* cox_autorelease(void* obj) {
        if (autopool == NULL) fprintf(stderr, "no autorelease pool in place, leaking memory\n");
        else {
                if (autopool->count == autopool->capacity) {
                        autopool->capacity *= 2;
                        autopool->objs = realloc(autopool->objs, sizeof(void*) * autopool->capacity);
                }
                autopool->objs[autopool->count++] = obj;
        }
        return obj;
}

Chceme-li tedy z nějaké funkce bezpečně vrátit nějaký objekt, stačí použít:

cox_my_object_t my_function() {
        cox_my_object_t obj = ...;
        ...
        return cox_autorelease(obj);
}

Uvedená metoda má pochopitelně svou režii, jako všechny způsoby automatické správy paměti, nicméně výhody plně převažují nad nevýhodami, zvláště pro kód vyšší úrovně, jehož autoři s výše uvedenými funkcemi pro práci s čítačem referencí nepřijdou do styku, protože jejich volání bude schované v příslušných knihovnách.

Zde představený způsob správy paměti používá například knihovna Grand Central Dispatch nebo - v poněkud abstraktnější formě - Windows Runtime (WinRT).


 

 

Článek pro vás napsal Petr Homola
Avatar
Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!
Aktivity (2)

 

 

Komentáře

Avatar
Martin Dráb
Redaktor
Avatar
Martin Dráb:13. dubna 13:29

Na práci s čítačem referencí ve více vláknovém prostředí by ideálně měly stačit atomické operace podporované procesorem (v přostředí WIndows třeba "funkce" InterlockedIn­crement a InterlockedDe­crement), mutex či něco podobně složitého není potřeba (GCC má zase své pseudofunkce).

V ukázkovém zdrojáku by asi bylo ideální ošetřit i případ, že se alokace nepodaří, pokud se již jedná o automatický systém pro správu paměti. ALe to je asi zřejmé.

Počítání referencí mám velmi rád, bohužel ale má problém s cykly.

Odpovědět  +1 13. dubna 13:29
2 + 2 = 5 for extremely large values of 2
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 1 zpráv z 1.