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

2023-05-28 0 422

难题 Demo

在多处理器下与此同时给函数调用表达式时能出现崩盘:

static NSObject *_instance; – (void)foo { _instance = [[NSObjectalloc] 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 引用计数为 1B、C 线程再开始执行,执行到 oldValue = _instance 时,会从 _instance函数调用 中读到 A 线程创建的对象,表达式给各自的 oldValue,oldValue 引用计数为 1B 线程在 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; }
为什么多线程下给全局变量赋值会崩溃
局部变量不会有这个难题
为什么多线程下给全局变量赋值会崩溃
为什么多线程下给全局变量赋值会崩溃

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

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务