多线程访问属性导致崩溃问题

4,144 阅读3分钟

背景

最近遇到线上一个偶现的崩溃,简化一下问题的模型是这样的:

@protocol SceneDelegate<NSObject>
- (nullable NSData *)onSceneRequest;
@end
@interface MyScene : NSObject<SceneDelegate>
@end
@implementation MyScene
- (nullable NSData *)onSceneRequest {
    return [NSData new];
}
@end
@interface ViewController ()
@property(nonatomic, strong) id<SceneDelegate> scene;
@end
@implementation ViewController
- (void)setScene:(id<SceneDelegate>scene {
    self.scene = scene;
}
- (void)onRequest {
    if (self.scene) {
        NSData *data = [self.scene onSceneRequest];
        NSLog(@"%@", data);
    }
}
@end

崩溃的点在[self.scene onSceneRequest]; 崩溃的类型是BAD_ACCESS(SIGBUS);

定位的过程

一开始并没有什么头绪,开始在网上扒SIGBUS崩溃的相关资料,找到了Apple的文章 Investigating Memory Access Crashes

其中有一段:

Consult the crashed thread’s backtrace for clues on where the memory access issue is occurring. Some types of memory access issues, such as dereferencing a NULL pointer, are easy to identify when looking at the backtrace and comparing it to the source code. Other memory access issues are identified by the stack frame at the top of the crashed thread’s backtrace:

  • If objc_msgSend, objc_retain, or objc_release is at the top of the backtrace, the crash is due to a zombie object. See Investigating Crashes for Zombie Objects.

对比一下崩溃的堆栈,这不正好就是objc_msgSend了么?

Apple如此笃定的认为,这就是一个僵死对象引起的?

然后,看代码并没有看出来哪里有什么僵尸对象啊。搜遍代码就只有一个setScene修改对象的状态,即使是设置了nil,也不应该是僵尸对象啊。

把焦点就围绕在setSceneonSceneRequest,代码深挖后,发现存在两个不同的线程分别调用这两个函数。似乎问题点就在这里了,ObjC和Java不同,对象等号=赋值并不一定是原子操作。

如何验证这个猜测是否正确的呢?只需要弄多几个线程,同时执行者两个方法,密集的模拟一下:

- (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 2; i++) {
        [NSThread detachNewThreadWithBlock:^{
            while(true) {
                [self setScene:[MyScene new]];
                [NSThread sleepForTimeInterval:0.3];
            }
        }];
    }
    for (int i = 0; i < 10; i++) {
        [NSThread detachNewThreadWithBlock:^{
            while(true) {
                [self onRequest];
                [NSThread sleepForTimeInterval:0.5+(CGFloat)i/10.0];
            }
        }];
    }
}

创建两个线程,每隔0.3s就调用setScene,创建10个不同的线程,以不同的时间间隔不断的调用onRequest。 好样的,跑起来,不到1分钟,就出现一个崩溃了。 好,就是这个原因了,修改方法就是atomic,对,就那么简单:)

@property(atomic, strong) id<SceneDelegate> scene;

背后的原因

为何对属性赋值的操作不是原子性的呢?

ObjC的运行时的各个方法都是没有上锁的,崩溃的交互可能是这样的:

所以,属性的访问修饰里才会有nonatomic和atomic的选择。很多时候,我们都只是默默的写nonatomic,而没有思考什么时候才需要atomic。

注意:atomic并不能解决多线程的竞争,它只能解决这种指针错误的崩溃。

关于nonatomic和atomic的区别,可以 参考这个文章

也可以阅读 Apple的文章

SO上也有一些 精彩的讨论

注意:上述文章都有讲述到一个问题,那就是属性的原子性只是保证访问属性对象的时候是线程安全的,并没有说访问属性对象内部数据是线程安全的。

如果存在多线程访问和修改属性内部的情况,还是要做额外的线程安全措施。

后记

头条也遇到了类似的问题,出了一个文章,更加深入的探讨,不只是属性,任意的对象在多线程环境下的读写都可能存在崩溃的情况。 这其实只是多线程编程的基本原则:多线程访问共享资源就需要有临界区的保护,任何非原子性读写所访问的数据都应该被理解为”共享资源“。

头条稳定性治理:ARC 环境中对 Objective-C 对象赋值的 Crash 隐患 - 掘金 (juejin.cn)