網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
問(wèn)題 Demo
在多線程下同時(shí)給全局變量賦值時(shí)會(huì)發(fā)生崩潰:
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
對(duì)匯編代碼進(jìn)行反匯編,可以看出 ARC 下編譯器添加了讀取舊值 oldValue = _instance 和釋放舊值 objc_release(oldValue) 的操作:
- (void)foo { NSObject *newValue = [[NSObject alloc] init]; NSObject *oldValue = _instance; //讀取舊值 _instance = newValue; objc_release(oldValue); //釋放舊值 }
給全局變量賦值時(shí)會(huì)讀取舊值、釋放舊值,舊值是從全局變量讀取的,多個(gè)線程可以同時(shí)讀到同一個(gè)值,如果一個(gè) 線程 在訪問(wèn)舊值時(shí),舊值被其它線程銷毀,就會(huì)發(fā)生崩潰。
即使在代碼中添加了判空邏輯,也會(huì)有可能多個(gè)線程同時(shí)進(jìn)入 if (!_instance) 里,發(fā)生錯(cuò)誤:
static NSObject *_instance; - (void)foo { if (!_instance) { _instance = [[NSObject alloc] init]; } }
即使不崩潰,多個(gè)線程也會(huì)產(chǎn)生不同的實(shí)例,是不符合預(yù)期的
崩潰路徑
可以推斷出一種復(fù)現(xiàn)崩潰的辦法:
- A B C 線程同時(shí)進(jìn)入
- (NSObject *)foo
方法 - A 線程先創(chuàng)建 NSObject 實(shí)例,賦值給 _instance (_instance = newValue),_instance 引用計(jì)數(shù)為 1
- B、C 線程再開(kāi)始執(zhí)行,執(zhí)行到 oldValue = _instance 時(shí),會(huì)從 _instance 全局變量中讀到 A 線程創(chuàng)建的對(duì)象,賦值給各自的 oldValue,oldValue 引用計(jì)數(shù)為 1
- B 線程在 objc_release(oldValue) 后會(huì)釋放 oldValue,oldValue 引用計(jì)數(shù)為 0,oldValue 被銷毀
- C 線程在 objc_release(oldValue) 時(shí)訪問(wèn) oldValue,發(fā)生崩潰
驗(yàn)證方式
lldb 的 thread continue 指令可以控制僅一個(gè)線程執(zhí)行,其它線程保持掛起。
利用該指令,可以復(fù)現(xiàn)崩潰路徑,按下面步驟可以驗(yàn)證:
- 準(zhǔn)備三個(gè)線程執(zhí)行
[self foo]
,并在-foo
方法里面打上斷點(diǎn):
可以多次測(cè)試讓 3 個(gè) 線程 同時(shí)進(jìn)入斷點(diǎn),進(jìn)入斷點(diǎn)后可以看到 Thread 2、3、4 是創(chuàng)建的 3 個(gè)線程:
不加 asm("nop\n") 的話執(zhí)行完 objc_release(oldValue) 后,foo 函數(shù)會(huì)直接結(jié)束,不太方便在 objc_release(oldValue) 之后打斷點(diǎn)進(jìn)行調(diào)試,添加之后 objc_release 之后會(huì)有位置打斷點(diǎn)(第 4 5 步用到)
- 在 Thread 2 中給匯編代碼第 10 行打斷點(diǎn),并執(zhí)行
thread contine
,使 Thread 2 執(zhí)行完 _instance = newValue :
可以看到 Thread 2 創(chuàng)建的實(shí)例為 0x0000000280df8020
- 使 Thread 3、4 線程 執(zhí)行完 oldValue = _instance
步驟1:刪除斷點(diǎn)(每次切換線程都要?jiǎng)h掉斷點(diǎn),不然 Xcode 可能會(huì)有 bug ...),切換到 Thread 3 ,給第 9 行打斷點(diǎn),并執(zhí)行 thread continue:
在 Xcode Debug Navitor 中選擇線程堆棧可以切換線程
或者使用 lldb,thread select 3
切換線程
步驟2:刪除斷點(diǎn),切換到 Thread 4,給第 9 行打斷點(diǎn),并執(zhí)行 thread continue
:
可以發(fā)現(xiàn) Thread 3、4 讀到的舊值都是 Thread 2 創(chuàng)建的 0x0000000280df8020
- 使Thread 3 執(zhí)行完 objc_release(oldValue)
步驟:刪除斷點(diǎn),切換到 Thread 3,給第 12 行打斷點(diǎn),并執(zhí)行 thread continue
:
此時(shí) oldValue 引用計(jì)數(shù)為 0,被銷毀
- 使 Thread 4 執(zhí)行 objc_release(oldValue), 訪問(wèn) oldValue
步驟:刪除斷點(diǎn),切換到 Thread 4,給第 12 行打斷點(diǎn),并執(zhí)行 thread continue
:
在 Thread 3 執(zhí)行 objc_release(oldValue) 后 oldValue 就已經(jīng)被銷毀了,
Thread 4 再次訪問(wèn)時(shí)會(huì)發(fā)生崩潰
其它測(cè)試
- 對(duì)成員變量賦值時(shí)同樣有這個(gè)問(wèn)題
@property (nonatomic, strong) NSObject *obj; - (NSObject *)getInstance { _obj = [[NSObject alloc] init]; return _obj; }
- 局部變量不會(huì)有這個(gè)問(wèn)題
局部變量不涉及"將舊值釋放"這個(gè)操作。
原文鏈接:https://juejin.cn/post/7112254387367608356
相關(guān)推薦
- 2022-07-03 Python基礎(chǔ)教程之錯(cuò)誤和異常的處理方法_python
- 2023-03-11 React中的for循環(huán)解讀_React
- 2022-04-26 jquery實(shí)現(xiàn)表格行拖動(dòng)排序_jquery
- 2022-12-13 詳解如何魔改Retrofit實(shí)例_Android
- 2022-02-27 springboot引入依賴lombok但是@Data(lombok的一個(gè)注解)仍然爆紅
- 2022-06-24 SingleFlight模式的Go并發(fā)編程學(xué)習(xí)_Golang
- 2022-07-27 關(guān)于pytest結(jié)合csv模塊實(shí)現(xiàn)csv格式的數(shù)據(jù)驅(qū)動(dòng)問(wèn)題_python
- 2023-01-20 Python中用try-except-finally處理異常問(wèn)題_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支