본문 바로가기
개발 및 운영/Golang

[golang] CGO Callback 으로 객체 넘길 때 GC 문제

by Joseph.Lee 2023. 12. 11.

golang 의 장점이 GC 이지만, Windows API 등을 사용하며 Callback 을 사용해야 할 때 참 골치아픈 것이 GC 문제이다.

https://groups.google.com/g/golang-nuts/c/yNis7bQG_rY/m/yaJFoSx1hgIJ

위와 같은 논의들도 많고..

package main

/*
 #include <stdint.h>
 #include <stdio.h>
 #include <unistd.h>
 #include <pthread.h>
 extern void goNativeDone(void*);
__attribute__((weak))
 void* thread_func(void* p) {
     for (int i=0; i<3; i++) {
         usleep(500000);
         printf("[thread] Value: %08x\n", *((uint32_t*)p));
     }
     goNativeDone(p);
     return NULL;
 }
__attribute__((weak))
 void worker(unsigned long long p) {
     uint32_t *up = (uint32_t*)p;
     pthread_t t;
     printf("[worker] Value: %p %08x %08x %08x %08x %08x %08x %08x %08x\n", up, up[0], up[1], up[2], up[3], up[4], up[5], up[6], up[7]);
     pthread_create(&t, NULL, thread_func, (void*)p);
 }
*/
import "C"
import (
    "runtime"
    "sync"
    "time"
    "unsafe"
)

type GoObject struct {
    Value1 int32
    Value2 int32
    Buffer [1048576 * 32]byte

    ch chan bool
}

func (o *GoObject) keep() {
    <-o.ch
}

func dotest() {
    obj := &GoObject{
        ch: make(chan bool),
    }
    obj.Value1 = 0x12345678
    obj.Value2 = 0x22222222
    go obj.keep()
    //pinner.Pin(obj)
    C.worker(C.ulonglong(uintptr(unsafe.Pointer(obj))))
    runtime.GC()
    runtime.GC()
    time.Sleep(time.Second * 3)
}

//export goNativeDone
func goNativeDone(pointer unsafe.Pointer) {
    obj := (*GoObject)(pointer)
    close(obj.ch)
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func() {
            dotest()
            wg.Done()
        }()
    }
    wg.Wait()
}

위의 코드는 동작하지 않는다.

channel 을 이용한 꼼수인데 go-routine 으로 keep() 함수가 살아있음에도 불구하고 GC가 동작해 GoObject 는 제거된다.

(1.21 버전으로 테스트한 결과:)

[worker] Value: 0xc000100000 12345678 22222222 00000000 00000000 00000000 00000000 00000000 00000000
[worker] Value: 0xc002380000 12345678 22222222 00000000 00000000 00000000 00000000 00000000 00000000
[thread] Value: 00000000
[thread] Value: 00000000
[thread] Value: 00000000
[thread] Value: 00000000
[thread] Value: 00000000
panic: close of nil channel

worker 함수까지는 GC가 동작하지 않아서 당연히 데이터가 있지만, worker 함수가 나가고 GC가 동작한 다음, thread 에서 해당 객체를 바라보면 메모리가 제거되어 초기화 된것을 볼 수 있다.

func (o *GoObject) keep() {
  <-o.ch
  _ = o.ch // 동작 안함
    o.Value2 = 2 // 동작 함
}

위와 같이 더미값을 주면 동작하긴 하다..

사실 가장 좋은 방법은 global variable 에 객체를 남겨놓는게 안전할 것이다. 이를 위해서 runtime.Pinner 가 있다.

반응형

댓글