본문 바로가기
모던 C 언어/C언어 메모리관리

3. C언어 레퍼런스 카운팅 방법의 한계 및 팁 들

by 커널패닉 2021. 5. 21.
반응형

본 포스트는 https://www.kernelpanic.kr/35https://www.kernelpanic.kr/36를 읽었다는 것은 전제로 작성되었다. 앞의 두 포스트를 먼저 읽고 오기를 바란다.

 

레퍼런스 카운팅 방법의 한계

레퍼런스 카운팅은 C언어에서 쉽고 효율적으로 메모리 관리를 할 수 있는 방법이다. 이 방법을 사용하면 대부분의 메모리 누수 관련 버그를 회피할 수 있지만 회피할 수 없는 누수도 있다. 순환참조(Circular Reference) 이슈이다.

순환참조는 둘 혹은 그 이상의 객체가 서로를 참조하는 상황이다. 예를 들자면 A 객체가 B 객체를 참조하고, B 객체가 A 객체를 참조하는 상황이다. 조금 꼬아본다면 A 객체가 B 객체를 참조하고, B 객체가 C 객체를 참조하는 상황에서 C 객체가 A 객체를 참조하는 상황이 있다.(A -> B -> C -> A) 이처럼 순환참조가 발생하면 서로의 객체에 대한 레퍼런스 카운트는 1이상이 되기 때문에 객체가 해제되지 않고 메모리 누수가 발생한다.

좌: 순환참조로 메모리 누수 발생 전,우: 메모리 누수 발생

안타깝게도 순환참조 문제를 해결하거나 예방할 수 있는 궁극적인 솔루션은 없다. 오직 프로그래머가 주의깊게 코드를 작성하는 방법 밖에는 없다. 다만 잘 구조화된 코드라면 순환참조 문제는 생기기 어렵다. 잘 구조화된 코드는 곧 코드의 의존성이 일방향적이고 직관적으로 구성된 코드이다. (시중에 나온 객체지향 프로그래밍, 디자인 패턴을 다루는 책들에서 코드 의존성에 대해 상세하고 다루고 있다.) 따라서 순환참조 문제에 직면했다면 혹시 코드의 구조가 망가지지 않았는지 다시 한 번 살펴보고, 코드의 품질을 개선해 보도록 하자.

 

고급 레퍼런스 카운팅 방법을 위한 팁들

GCC의 도움받기

malloc 등으로 생성한 객체의 포인터는하나 이상의 변수가 관리해야 한다. 당연한 소리겠지만 포인터 주소를 잊어버리면 동적으로 할당된 메모리를 해제할 수 없기 때문이다. 하지만 프로젝트가 커지고 개발자가 늘어나면 초기화 함수로 생성한 객체를 변수로 받지 않는 경우도 발생할 수 있다.

struct Team {
    // .. < 생략 > ...
};

struct Team* create_team() {
    struct Team *team = malloc(sizeof(*team));
    if (team == NULL) goto err;
    
    // ... < 중략 > ...

    return team;
}

int main() {
    // 생성된 Team 객체의 포인터를 할당받지 않아서
    // 메모리 누수가 발생한다.
    create_team();
    
    // ... < 하략 > ...
}

초기화한 객체의 포인터 주소를 할당받지 않아서 동적으로 할당된 메모리 포인터가 누실되었다. 흔한 일은 아니겠지만 휴먼 에러는 언제든지 발생할 수 있다. 이런 경우 GCC의 도움을 받을 수 있다. GCC가 제공하는 warn_unused_result를 사용하면 객체의 포인터를 받지 않으면 컴파일 시에 경고가 발생한다. (물론 컴파일 경고를 읽지 않는다면 아무 소용이 없겠지만...)

아래는 warn_unused_result를 사용한 예이다.

struct Team {
    // .. < 생략 > ...
};

// return 값이 사용되지 않는 경우 경고를 띄우는 매크로
__attribute__((warn_unused_result)) struct Team* create_team() {
    struct Team *team = malloc(sizeof(*team));
    if (team == NULL) goto err;
    
    // ... < 중략 > ...

    return team;
}

int main() {
    // return 값을 받지 않으면 컴파일 시 경고가 나온다.
    create_team();
    
    // ... < 하략 > ...
}
rc.c: In function ‘main’:
rc.c:148:5: warning: ignoring return value of ‘create_team’ declared with attribute ‘warn_unused_result’ [-Wunused-result]
  148 |     create_team();
      |     ^~~~~~~~~~~~~~~~

 

멀티쓰레드에 안전한 레퍼런스 카운트 사용하기

앞선 포스트에서 살펴본 레퍼런스 카운트(이하 rc) 구현은 멀티쓰레드 상황에서 안전하지 않다. (참조: https://ko.wikipedia.org/wiki/%EC%8A%A4%EB%A0%88%EB%93%9C_%EC%95%88%EC%A0%84) 멀티쓰레드에서 안전하려면 rc에 Lock을 거걸면 된다. Lock을 사용한 구현은 아래와 같다.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>

#define OBJECT(x) ((struct Object*)x)

pthread_mutex_t RC_LOCK;

struct Object {
    unsigned int ref_count;
    void (*free)(void* obj);
};

// Object를 초기화 하는 함수를 만드는 것이 편리하다.
void init_object(struct Object *obj, void (*free)(void*)) {
    pthread_mutex_lock(&RC_LOCK);   // lock
    obj->ref_count = 0;
    pthread_mutex_unlock(&RC_LOCK);  // unlock
    obj->free = free;
}

void ref(struct Object *obj) {
    if (obj == NULL) return;
    pthread_mutex_lock(&RC_LOCK);  // lock
    obj->ref_count += 1;
    pthread_mutex_unlock(&RC_LOCK); // unlock
}

void unref(struct Object *obj) {
    if (obj == NULL) return;
    unsigned int rc;
    pthread_mutex_lock(&RC_LOCK); // lock 
    obj->ref_count -= 1;
    rc = obj->ref_count;
    pthread_mutex_unlock(&RC_LOCK);  // unlock
    if (rc == 0) {
        if (obj->free) obj->free(obj);
        else free(obj);
    }
}

// ... < 중략 > ...

int main() {
    pthread_mutex_init(&RC_LOCK, NULL); // lock 초기화
    
    // ... < 생략 > ...
}

Lock을 사용해도 원하는대로 쓰레드 안전한 rc를 사용할 수 있다. 그러나 gcc의 확장 함수를 사용하면 좀더 쉽게 쓰레드 안전한 rc를 만들 수 있다. gcc에서는 __atomic_fetch_add라는 atomic 처리를 위한 확장함수를 제공한다. 이를 이용하면 아래와 같이 쓰레드 안전한 함수를 만들 수 있다.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define OBJECT(x) ((struct Object*)x)

// ref_count를 atomic으로 처리하기 위한 매크로 함수
// __atomic_fetch_add: gcc에서 제공하는 Atomic을 위한 확장함수
#define atomic_int_add(atomic, val) \
    (int) __atomic_fetch_add((atomic), (val), __ATOMIC_SEQ_CST);

struct Object {
    unsigned int ref_count;
    void (*free)(void* obj);
};

// Object를 초기화 하는 함수를 만드는 것이 편리하다.
void init_object(struct Object *obj, void (*free)(void*)) {
    obj->ref_count = 0;
    obj->free = free;
}

void ref(struct Object *obj) {
    if (obj == NULL) return;
    atomic_int_add(&obj->ref_count, 1);
}

void unref(struct Object *obj) {
    if (obj == NULL) return;
    int old_rc = 0;
    old_rc = atomic_int_add(&obj->ref_count, -1);
    if (old_rc == 1) {
        if (obj->free) obj->free(obj); // 개별 해제 함수가 있으면 사용
        else free(obj);                // 없으면 free() 함수 사용
    }
}

// ... < 생략 > ...
반응형