首頁 > 軟體

iOS開發多執行緒下全域性變數賦值崩潰原理詳解

2022-07-20 22:01:42

問題 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("nopn") 的話執行完 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;
}

  • 區域性變數不會有這個問題

區域性變數不涉及"將舊值釋放"這個操作。

以上就是iOS開發多執行緒下全域性變數賦值崩潰原理詳解的詳細內容,更多關於多執行緒全域性變數賦值崩潰的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com