問題 Demo
在多執行緒下同時給全域性變數賦值時會發生崩潰:
static NSObject *_instance;- (void)foo { _instance = [[NSObject alloc] init];}
崩潰原因
如下為原始碼的彙編程式碼:
Demo-iOS`-[ViewController foo]: 0x104e4e088 <+0>: stp x29, x30, [sp, #-0x10]! 0x104e4e08c <+4>: mov x29, sp # newValue = [[NSObject alloc] init] 0x104e4e094 <+12>: ldr x0, #0x7454 ; (void *)0x00000001db209e08: NSObject 0x104e4e098 <+16>: bl 0x104e4e438 ; symbol stub for: objc_alloc_init # oldValue = _instance 0x104e4e09c <+20>: adrp x9, 7 0x104e4e0a0 <+24>: ldr x8, [x9, #0x788] # _instance = newValue 0x104e4e0a4 <+28>: str x0, [x9, #0x788] # objc_release(oldValue) 0x104e4e0a8 <+32>: mov x0, x8 0x104e4e0ac <+36>: ldp x29, x30, [sp], #0x10 0x104e4e0b0 <+40>: b 0x104e4e480 ; symbol stub for: objc_release
對彙編程式碼進行反彙編,可以看出 ARC 編譯器添加了讀取舊值 oldValue = _instance 和釋放舊值 objc_release(oldValue) 的操作:
- (void)foo { NSObject *newValue = [[NSObject alloc] init]; NSObject *oldValue = _instance; //讀取舊值 _instance = newValue; objc_release(oldValue); //釋放舊值}
給全域性變數賦值時會
讀取舊值
、
釋放舊值
,
舊值是從全域性變數讀取的,多個執行緒可以同時讀到同一個值,
如果一個
執行緒
在訪問舊值時,舊值被其它執行緒銷燬,就會發生崩潰
。
即使在程式碼中添加了判空邏輯,也會有可能多個執行緒同時進入 if (!_instance) 裡,發生錯誤:
static NSObject *_instance;- (void)foo { if (!_instance) { _instance = [[NSObject alloc] init]; }}
即使不崩潰,多個執行緒也會產生不同的例項,是不符合預期的
崩潰路徑
可以推斷出一種復現崩潰的辦法:
A B C 執行緒同時進入
- (NSObject *)foo
方法
A 執行緒先建立 NSObject 例項,賦值給 _instance (_instance = newValue),_instance 引用計數為 1
B、C 執行緒再開始執行,執行到 oldValue = _instance 時,會從 _instance
全域性變數
中讀到 A 執行緒建立的物件,賦值給各自的 oldValue,oldValue 引用計數為 1
B 執行緒在 objc_release(oldValue) 後會釋放 oldValue,oldValue 引用計數為 0,oldValue 被銷燬
C 執行緒在 objc_release(oldValue) 時訪問 oldValue,發生崩潰
驗證方式
lldb 的 thread continue 指令 可以控制僅一個執行緒執行,其它執行緒保持掛起。
利用該指令,可以復現崩潰路徑,按下面步驟可以驗證:
準備三個執行緒執行
[self foo]
,並在
-foo
方法裡面打上斷點:
可以多次測試
讓 3 個
執行緒
同時進入斷點
,進入斷點後可以看到 Thread 2、3、4 是建立的 3 個執行緒:
不加
asm
(“nop\n”) 的話執行完 objc_release(oldValue) 後,foo 函式會直接結束,不太方便在 objc_release(oldValue) 之後打斷電話進行除錯,新增之後 objc_release 之後會有位置打斷點(第 4 5 步用到)
在 Thread 2 中給彙編程式碼第 10 行動打斷電話,並執行
thread contine
,
使 Thread 2 執行完
_instance = newValue
:
可以看到 Thread 2 建立的例項為 0x0000000280df8020
使 Thread 3、4
執行緒
執行完
oldValue = _instance
步驟1:刪除斷點(每次切換執行緒都要刪掉斷點,不然 Xcode 可能會有 bug 。。。),切換到 Thread 3 ,給第 9 行動打斷電話,並執行 thread continue:
在 Xcode Debug Navitor 中選擇執行緒堆疊可以切換執行緒
或者使用 lldb,
thread select 3
切換執行緒
步驟2:刪除斷點,切換到 Thread 4,給第 9 行動打斷電話,並執行
thread continue
:
可以發現 Thread 3、4 讀到的舊值都是 Thread 2 建立的 0x0000000280df8020
使Thread 3 執行完
objc_release(oldValue)
步驟:刪除斷點,切換到 Thread 3,給第 12 行動打斷點,並執行
thread continue
:
此時 oldValue 引用計數為 0,被銷燬
使 Thread 4 執行 objc_release(oldValue), 訪問 oldValue
步驟:刪除斷點,切換到 Thread 4,給第 12 行打斷點,並執行
thread continue
:
在 Thread 3 執行 objc_release(oldValue) 後 oldValue 就已經被銷燬了,
Thread 4 再次訪問時會發生崩潰
其它測試
對成員變數賦值時同樣有這個問題
@property (nonatomic, strong) NSObject *obj;- (NSObject *)getInstance { _obj = [[NSObject alloc] init]; return _obj;}
區域性變數不會有這個問題
區域性變數不涉及“將舊值釋放”這個操作。
猜你喜歡
- 2023-01-13帶你走進Linux核心原始碼中最常見的資料結構之「mutex」
- 2023-01-03記一次生產中使用CompletableFuture遇到的坑
- 2022-12-10Java非同步程式設計(5種非同步實現方式詳解)
- 2021-06-30i5 10600KF還是i710700,誰才是為遊戲玩家量體裁衣的最佳選擇?
- 2021-04-20為什麼這麼多年了,intel和AMD的CPU頻率還是沒有超過5Ghz?