问题 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];
    }
}

即使不溃散,多个线程也会发生不同的实例,是不符合预期的

溃散途径

可以推断出一种复现溃散的方法:

  1. A B C 线程一同进入 - (NSObject *)foo 方法
  1. A 线程先创建 NSObject 实例,赋值给 _instance (_instance = newValue),_instance 引用计数为 1
  1. B、C 线程再初步实行,实行到 oldValue = _instance 时,会从 _instance 全局变量中读到 A 线程创建的目标,赋值给各自的 oldValue,oldValue 引用计数为 1
  1. B 线程在 objc_release(oldValue) 后会开释 oldValue,oldValue 引用计数为 0,oldValue 被销毁
  1. C 线程在 objc_release(oldValue) 时拜访 oldValue,发生溃散

验证方式

lldb 的 thread continue 指令可以控制仅一个线程实行,其它线程保持挂起。

利用该指令,可以复现溃散途径,按下面进程可以验证:

  1. 准备三个线程实行 [self foo],并在 -foo 方法里面打上断点

为什么多线程下给全局变量赋值会溃散

可以屡次测验让 3 个 线程 一同进入断点,进入断点后可以看到 Thread 2、3、4 是创建的 3 个线程:

为什么多线程下给全局变量赋值会溃散

不加 asm(“nop\n”) 的话实行完 objc_release(oldValue) 后,foo 函数会直接完毕,不太方便在 objc_release(oldValue) 之后打断点进行调试,添加之后 objc_release 之后会有方位打断点(第 4 5 步用到)

为什么多线程下给全局变量赋值会溃散
为什么多线程下给全局变量赋值会溃散

  1. 在 Thread 2 中给汇编代码第 10 行打断点,并实行 thread contine使 Thread 2 实行完 _instance = newValue

为什么多线程下给全局变量赋值会溃散

可以看到 Thread 2 创建的实例为 0x0000000280df8020

  1. 使 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

  1. 使Thread 3 实行完 objc_release(oldValue)

进程:删去断点,切换到 Thread 3,给第 12 行打断点,并实行 thread continue:

为什么多线程下给全局变量赋值会溃散

此刻 oldValue 引用计数为 0,被销毁

  1. 使 Thread 4 实行 objc_release(oldValue), 拜访 oldValue

进程:删去断点,切换到 Thread 4,给第 12 行打断点,并实行 thread continue:

为什么多线程下给全局变量赋值会溃散

在 Thread 3 实行 objc_release(oldValue) 后 oldValue 就已经被销毁了,

Thread 4 再次拜访时会发生溃散

其它测验

  1. 对成员变量赋值时相同有这个问题
 @property (nonatomic, strong) NSObject *obj;
- (NSObject *)getInstance {
    _obj = [[NSObject alloc] init];
    return _obj;
}

为什么多线程下给全局变量赋值会溃散

  1. 局部变量不会有这个问题

为什么多线程下给全局变量赋值会溃散
为什么多线程下给全局变量赋值会溃散

局部变量不触及”将旧值开释”这个操作。