文章目錄
- 背景
- 測試代碼
- 運行測試
- 嘗試打開編譯器優化
- 進一步分析
背景
業務中出現日志打印失效,發現是因為管理日志對象的單例在運行過程中存在了多例的情況。下面通過還原業務場景來分析該問題。
測試代碼
/* 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中。