日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無(wú)先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

iOS開(kāi)發(fā)多線程下全局變量賦值崩潰原理詳解_IOS

作者:No ? 更新時(shí)間: 2022-09-14 編程語(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

欄目分類
最近更新