文章目录
- 背景
- 测试代码
- 运行测试
- 尝试打开编译器优化
- 进一步分析
背景
业务中出现日志打印失效,发现是因为管理日志对象的单例在运行过程中存在了多例的情况。下面通过还原业务场景来分析该问题。
测试代码
/* A.h */
#ifndef CALSS_A
#define CALSS_A#include <iostream>
#include <cstddef>
class A {
public:static A& GetInstance();void SetNum(size_t num);size_t GetNum();private:size_t m_num {0U};
};
#endif
/* A.cpp */
#include "A.h"
A& A::GetInstance()
{static A ins;std::cout << "A " << &ins << std::endl;return ins;
}void A::SetNum(size_t num)
{m_num = num;
}size_t A::GetNum()
{return m_num;
}
/* A2.h */
#ifndef CALSS_A_2
#define CALSS_A_2#include <iostream>
#include <cstddef>class A {
public:static A& GetInstance(){static A ins2;std::cout << "A2 " << &ins2 << std::endl;return ins2;}void SetNum(size_t num) {m_num = num;}size_t GetNum(){return m_num;}private:size_t m_num {0U};
};
#endif
/* B.h */
#ifndef CLASS_B
#define CLASS_B#include <cstddef>class B {
public:B();size_t GetNum();
};#endif
/* B.cpp */
#include "B.h"
#include "A2.h"B::B()
{A::GetInstance().SetNum(100U);
}size_t B::GetNum()
{return A::GetInstance().GetNum();
}
#include "A.h"
#include "B.h"
#include <iostream>int main()
{B b;A::GetInstance().SetNum(10U);std::cout << A::GetInstance().GetNum() << std::endl;std::cout << b.GetNum() << std::endl;return 0;
}
运行测试
通过简化的代码模拟业务中实际的依赖关系:头文件A.h中定义了类A,单例的实现在A.cpp中,生成动态库a;头文件A2.h中同样也定义了类A,单例的视线在头文件中,被B.cpp引用,生成动态库b;可执行文件a.out中会同时调用动态库a和动态库b中的接口,在实际业务中出现了多例的情况。
g++ A.cpp -I . -fpic -shared -o liba.so -O2
g++ B.cpp -I . -fpic -shared -o libb.so -O2
g++ main.cpp -L . -lb -la -I . -O2
运行结果显示,只存在单例,获取到的是A2.h中定义的对象(libb.so)。
A2 0x7fbcdfb19068
A2 0x7fbcdfb19068
A2 0x7fbcdfb19068
10
A2 0x7fbcdfb19068
10
调整二进制动态库链接的顺序,获取到的是A.cpp中定义的对象(liba.so)。从目前测试情况分析,不会出现多例的情况,但是具体使用的符号,跟动态库链接的顺序有关系,二进制中会使用先链接的动态库的符号。
g++ main.cpp -L . -la -lb -I .
A 0x7f99ef74f058
A 0x7f99ef74f058
A 0x7f99ef74f058
10
A 0x7f99ef74f058
10
从符号表分析:使用readelf读取动态库和二进制的符号表,动态库b中既存在单例获取成员函数A::GetInstance()的弱符号,又存在全局唯一对象A::GetInstance()::ins2的符号。结合上述现象,先链接动态库b时,加载全局唯一对象A::GetInstance()::ins2的内存地址,后续获取到的单例都是该内存地址;后链接动态库b,弱符号A::GetInstance()会被a库中的强符号覆盖,因此获取到的单例是A.cpp中定义的对象。
尝试打开编译器优化
前面证明链接时候的顺序不同,会加载不同内存地址的对象,但是在运行过程中还是单例。现在猜测运行过程中出现多例情况可能跟编译器的优化有关。因此,舱室打开编译的优化选项,重读上面的测试。
g++ A.cpp -I . -fpic -shared -o liba.so -O2
g++ B.cpp -I . -fpic -shared -o libb.so -O2
g++ main.cpp -L . -lb -la -I . -O2
运行结果显示,出现了多例的现象。
A2 0x7f019f611068
A 0x7f019f60c068
A 0x7f019f60c068
10
A2 0x7f019f611068
100
从符号表分析:与未打开编译器优化前最大的区别在于动态库b中单例获取成员函数A::GetInstance()的弱符号不见了,故动态库b中源码加载全局唯一对象A::GetInstance()::ins2的内存地址,动态库a中源码加载的是通过A::GetInstance()获取的对象的地址,两者地址不同。
因此,可以解释为什么在运行过程中出现了双例的情况。
进一步分析
动态库b中单例获取成员函数A::GetInstance()的弱符号不见了的原因:
头文件中定义的函数,特别是内联函数和模板函数,在编译和链接过程中通常会被展开或优化掉,不会产生独立的符号。
无论是链接时会存在双例的情况,还是运行时会存在双例的情况,都是不符合预期的。因此,如何避免?
很简单,单例的实现放在cpp中。