IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

Automatická správa pamäte v C

Hoci je C nízkoúrovňový jazyk bez moderných vymožeností, je možné (a žiaduce) spravovať v ňom pamäť automaticky. V tomto článku si predstavíme osvedčený spôsob správy pamäte v C založený na počítanie referencií.

C nie je objektovo orientované, nemá teda dedičnosť ani polymorfizmus. Kým dedičnosť nie je nevyhnutne potrebná (pozri napr. Go), polymorfizmu sa v niektorých prípadoch nemožno vyhnúť. Naša malá knižnica pre prácu s objektmi v C (COX = C Object Extensions) si teda musí pre každý typ objektu ( "triedu") držať v pamäti adresu na funkcie zdieľané viac objekty, ak sa pre rôzne typy objektov správajú inak. V našom jednoduchom prípade budeme mať len funkciu (resp. Metódu) pre mobilizáciu objektu a prevod na textovú reprezentáciu (pre ladenie):

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

Každý objekt bude mať hlavičku obsahujúce odkaz na typ a počítadlo referencií:

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

Ak chceme napríklad automaticky spravovaný objekt pre reťazce, deklarujeme ho takto:

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

Každá inštancia teda okrem vlastného reťazca (cstr) a jeho dĺžky (ľan) obsahuje aj informácie o type a čítač referencií. Typ je definovaný nasledovne:

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

Uvoľnenie reťazca je jednoduché, najskôr vrátime pamäť alokovanú pre cstr a potom objekt samotný. Popis objektu je ešte jednoduchšie, reťazec totiž v tomto prípade vracia seba samého:

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;
}

Nie príliš zložité je tiež vytvorenie objektu reťazca:

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čí alokovať pamäť, priradiť informáciu o type, čítač referencií nastaviť na 1 a nakoniec alokovať pamäť pre cstr a text skopírovať.

Ak už objekt nepoužívame, uvoľníme ho. Pre ľubovoľný objekt implementovaný pomocou našej knižnice budeme pre uvoľnenie používať funkciu 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);
    }
}

Táto funkcia zníži čítač referencií a pokiaľ tento klesne na nulu, tj. Na objekt neexistujú žiadne ďalšie referencie, zavolá sa deštruktor z informácie o type, čím sa uvoľnia všetky prostriedky používané objektom (vrátane pamäte pre objekt samotný).

Všimnite si funkcií cox_refcount_lock() a cox_refcount_unlock(). Tie sú dôležité pri aktualizácii čítača referencií vo viacvláknové prostredí. Ich implementácia závisí na konkrétnom operačnom systéme, najjednoduchšie je použiť mutex alebo semafor (napr. Vo Windows funkcii CreateSemaphore atď.).

Ak teda vytvoríme reťazec a po použití na neho zavoláme cox_release(), objekt sa automaticky uvoľní:

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

Ak takýto objekt pridáme napr. Do nejakej kolekcie, táto kolekcia zvýši jeho čítač referencií a cox_release objekt neodstráni z pamäte, pretože ešte existuje ďalšie referencie.

Aby sme nemuseli explicitne volať cox_release() vo všetkých vetvách kódu, deklarujeme si atribút __auto:

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

Potom môžeme deklarovať premenné takto:

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

vďaka čomu nie je nutné použitie cox_release(), pretože taká premenná sa automaticky uvoľní (resp. zníži sa jej počítadlo referencií) po opustení jej rozsahu platnosti.

Atribút __auto je užitočný pre lokálne premenné, problém ale nastáva, ak chceme vrátiť objekt z funkcie, bez toho aby sme sa museli starať o jeho uvoľnení, lebo návratom z funkcie opúšťame rozsah platnosti premenných. Pre tento účel sa hodia 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á svoj zásobník poolov, do ktorých ukladá objekty určené k neskoršiemu (automatickému) uvoľnenie. Typicky teda budeme mať:

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

pričom pool za nás zvyčajne spravuje slučka udalostí alebo nejaký podobný mechanizmus, takže sa o explicitné spravovanie poolov starať nemusíme. Príslušná mašinéria je definovaná 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;
}

Ak chceme teda z nejakej funkcie bezpečne vrátiť nejaký objekt, stačí použiť:

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

Uvedená metóda má pochopiteľne svoju réžiu, ako všetky spôsoby automatickej správy pamäti, však výhody plne prevažujú nad nevýhodami, zvlášť pre kód vyššej úrovne, ktorého autori s vyššie uvedenými funkciami pre prácu s čítačom referenciou neprídu do styku, pretože ich volanie bude schované v príslušných knižniciach .

Tu predstavený spôsob správy pamäte používa napríklad knižnica Grand Central Dispatch alebo - v trochu abstraktnejšie forme - Windows Runtime (WinRT).


 

Predchádzajúci článok
Funkcie s variabilným počtom a Typo argumentovať (stdarg.h)
Všetky články v sekcii
Pokročilé konštrukcia jazyka C
Článok pre vás napísal Petr Homola
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje HPC a umělé inteligenci
Aktivity