Leaky singletons的一种使用场景
文章目录
- Leaky singletons的一种使用场景
- 场景
- 问题本质
- 如何解决
- Leaky singletons
场景
最近遇到了这个问题,正好想记录下。
比如你有一段代码,如下(伪代码):
static std::map<int, std::string> g_test_map;class Test {public:~Test() {} // 析构里对线程做join动作bool Init() {} // 启动多个线程对g_test_map进行读写操作
};
你在一个文件中定义了一个static map静态变量,并且你的代码被编译成test.so。
重点来了,这个test.so是以共享库的形式被别人使用,而且是通过dlopen、dlclose的形式加载和卸载你的test.so。
使用方代码如下(伪代码):
std::shared_ptr<Test> LoadTest() {dlopen("./test.so");std::shared_ptr<Test> test = std::make_shared<Test>();test->Init();return test;
}UnLoadTest() {test.reset();dlclose("./test.so");
}
使用方1在dlopen你的test.so的时候,会申请出这个static map的内存空间,你的test.so中会另外启动多个线程在对这个map进行读写操作。并且会将Test的shared_ptr返回给使用方2持有。
这里就会形成如下关系图:
使用方2->(load) 使用方1的.so -> 使用方1 (dlopen) test.so。
那在使用方2 退出的时候,使用方1会先reset 它保存的test的shared_ptr,然后再dlclose。
这里就会出现问题:
- 使用方1在reset test的时候,只是会将test的引用技术减1,此时使用方2还持有着test,所以test并不会析构,也不会触发线程join;
- 紧接着使用方1开始 dlclose,并且dlclose的时候会将g_test_map析构掉;
- 此时由于test还未析构,所以test中的线程还在run,这个时候就会出现访问已析构的内存,产生coredump。
这个问题如果用asan跑下,可以很明显看出asan提示的
AddressSanitizer: heap-use-after-free on address
READ of size 8 at 0xffff8e408f90 thread T9:0xffff8e408f90 freed by thread T0 here:
这段的意思就是堆内存释放后又被使用,0xffff8e408f90 这个地址在T0被释放,又在T9被使用。
问题本质
所以综上所述,这个问题的本质是dlclose的时候静态全局变量被析构,但是存在running的线程还在使用这块全局内存。
这里有人会说,那dlclose的时候应该将test清理干净,这样就不会存在这种场景。这种说法没问题,但是这里Test的线程是在Test析构的时候join的,而Test是以shared_ptr的形式返回,并且又被使用方2持有着。所以这里场景下使用方1本质控制不了test的析构。
如何解决
- 第一种方法可以将static map封装成类,然后用shared_ptr包装起来,别的线程要读写这个map,就必须保存一份map的shared_ptr类,这样就能保存最后释放。
- 第二种方法也是之前遇到glog0.4中的一个问题,参考glog的修复手段。
glog中的问题也是进程退出的时候static mutex被释放了,但是别的线程还在使用这个mutex,可以看下这个:
glog0.4.0版本存在此问题,static google::log_mutex destroyed on exit, while other still existing threads want to log。具体可看如下链接:
https://github.com/google/glog/issues/504
glog里使用了Leaky singletons这种方式解决了此问题。
Leaky singletons
Leaky singletons很好理解,翻译成中文就是泄漏式单例,代码如下:
static Singleton& instance() {/** 以前的实现static Singleton inst;return inst;*/static Singleton* inst = new Singleton; // 返回指针而不是整个对象return *inst;
}
用静态指针替换了静态对象,这样进程启动只会new一份,并且不会手动释放,而是进程退出的时候被系统回收内存。
这里并不是真实泄露,最终内存也会随着进程退出被系统回收。