专栏首页落影的专栏iOS property的多线程问题解析

iOS property的多线程问题解析

正文

问题列表

1、以下这段代码,在主线程执行会输出什么?

// 属性
@property (nonatomic, strong) NSArray *myNumberArr; 

// 运行代码
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, self.myNumberArr.count); 
    }); 
} 

2、稍作修改,以下代码在主线程执行会输出什么?

// 属性
@property (atomic, strong) NSArray *myNumberArr; 

// 运行代码
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        _myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, _myNumberArr.count); 
    }); 
} 

3、换了个类型,以下代码在主线程执行会输出什么?

// 属性
@property (nonatomic, strong) NSNumber *myNumber; 

// 运行代码
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumber = @(i);
        NSLog(@"%d, count:%d", i, self.myNumber.intValue); 
    }); 
} 

问题分析

题目1
// 属性
@property (nonatomic, strong) NSArray *myNumberArr; 

// 运行代码
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, self.myNumberArr.count); 
    }); 
} 

先解析提供的要素: a.nonatomic的NSArray属性; b.异步执行,gcd并发队列; c.多个block,对myNumberArr的多次读写操作;

由a+b+c组成了一个多线程访问nonatomic属性的方法,如果直接运行会遇到下面的问题:

题目2

// 属性
@property (atomic, strong) NSArray *myNumberArr; 

// 运行代码
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        _myNumberArr = @[[NSString stringWithFormat:@"%d", i]]; 
        NSLog(@"%d, count:%d", i, _myNumberArr.count); 
    }); 
} 

myNumberArr的属性变为了atomic; 属性的访问,没有用.myNumberArr的getter方式,而是直接使用_myNumberArr访问;

如果直接运行,同样会遇到下面的问题:

题目3

// 属性
@property (nonatomic, strong) NSNumber *myNumber; 

// 运行代码
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        self.myNumber = @(i);
        NSLog(@"%d, count:%d", i, self.myNumber.intValue); 
    }); 
} 

与题目1类似: a.nonatomic的NSNumber属性; b.异步执行,gcd并发队列; c.多个block,对myNumber的多次读写操作;

由题目1的经验,由a+b+c组成了一个多线程访问nonatomic属性myNumber的方法,预期直接运行会遇到相同的多线程问题。但实际上是可以正常跑完,即使多尝试几次。

问题延伸

多线程问题出现原因

为了更好理解多线程读写属性的理解,我们以题目1为样例,假设其代码在-viewDidLoad方法。

// 运行代码
for (int i = 0; i < 1000; ++i) { 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{ 
        NSArray *arr = @[[NSString stringWithFormat:@"%d", i]];
        self.myNumberArr = arr; 
    }); 
} 

上面的代码其实是跑在ARC的环境下,而OC实现ARC就是编译时添加retain和release的方法。

我们将上面的ARC代码转成更原始的代码,得到更接近真实运行的代码。

转换方式:将ViewController.m的代码编译,得到ViewController.o文件,再进行还原得到类似的MRC代码。

比如说block中的代码会生成:

根据上图,给self.myNumberArr赋值的var_28已经进行过retain操作。

打开setMyNumberArr:方法,我们知道最终赋值的操作是通过objc_storeStrong来执行,这个方法如下:

void objc_storeStrong(id *object, id value) {
  id oldValue = *object;
  value = [value retain];
  *object = value;
  [oldValue release];
}

当我们给self.myNumberArr赋值时,除了需要retain传进来的值,还需要将self.myNumberArr原来的值进行release,否则赋值之后原来的self.myNumberArr值将成为野指针。

当我们有多个线程执行self.myNumberArr = arr,也就是会有多个线程同时跑到objc_storeStrong函数。 假设线程1和线程2同时运行objc_storeStrong,他们会同时拿到oldValue,此时retainCount都为1;当2个线程执行完赋值操作,都会对oldValue进行release操作,此时就会触发对象的重复release,造成崩溃。

nonatomic和atomic的区别

我们把myNumberArr的生命的nonatomic改成atomic,再试试看看生成的代码。

// 属性
@property (atomic, strong) NSArray *myNumberArr; 

结果如下:

objc_setProperty_atomic的代码如下: (注意看下面if(atomic)的分支)

void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset)
{
    reallySetProperty(self, _cmd, newValue, offset, true, false, false);
}

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:NULL];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:NULL];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
        _spin_lock(slotlock);
        oldValue = *slot;
        *slot = newValue;        
        _spin_unlock(slotlock);
    }

    objc_release(oldValue);
}

我们知道了atomic的属性在setter的时候,会通过加锁来保证赋值操作的原子性。

这样也解释了题目2的时候,为什么声明了atomic,但是通过_myNumberArr属性去操作会发生多线程问题。因为用下划线_myNumberArr访问属性时,不会经过getter/setter。

NSArray的多线程问题

有一个经典问题:NSArray是否为线程安全类,能否用atomic修饰NSArray属性保证属性的读写线程安全吗? 根据前面的分析,我们可以知道atomic可以保证getter/setter在使用的时候,不会出现多线程问题; 再根据官网的资料,我们知道NSArray是Thread Safety。 综上,答案是:NSArray是线程安全类,可以用atomic修饰NSArray属性保证属性的读写线程安全。

但是,是否涉及NSArray的操作,都不需要考虑多线程的问题? 看下面的一段代码:

@property (atomic, strong) NSArray<NSMutableDictionary *> *myAtomicArr;

// 运行代码
for (int i = 0; i < 1000; ++i) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        if ([self.myAtomicArr[0] objectForKey:@"testKey"]) {
            [self.myAtomicArr[0] removeObjectForKey:@"testKey"];
        }
        else {
            self.myAtomicArr[0][@"testKey"] = @"testStr";
        }
    });
}

通过前面的经验,我们容易知道这种代码会出现多线程问题。 我们能保证NSArray类的线程安全,但是无法保证NSArray内的属性操作是线程安全,所以在使用NSArray时,仍需要小心多线程问题。

思考题?:

为什么题目3可以正常运行?(答案见附录最后一篇)

总结

多线程的问题有很多场景,这里仅针对属性的多线程读写这个case进行分析,对多线程问题建立一个基础的认知。

附录

Thread Safety Summary objc_storeStrong objc_setProperty_atomic 一次标签指针(Tagged Pointer)导致的事故

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • iOS-多线程死锁问题

    虽然作者在开发中经常会用到多线程,但是对于死锁、线程阻塞等问题还是比较模糊,一般都是先写吧,有问题再改,现在闲下来了,是时候总结一波了,本文主要总结一下同步/异...

    用户2215591
  • FLAnimatedImage -ios gif图片加载框架介绍

    简介 FLAnimatedImage 是 Flipboard 团队开发的在它们 App 中渲染 GIF 图片使用的库。 后来 Flipboard 将 FLAni...

    xiangzhihong
  • FLAnimatedImage -ios gif图片加载框架介绍

    简介 FLAnimatedImage 是 Flipboard 团队开发的在它们 App 中渲染 GIF 图片使用的库。 后来 Flipboard 将 FLAni...

    xiangzhihong
  • 解决SCENIC流程的多线程问题

    一般来说,遇到了这样的报错说明其实并没有follow我的教程,而是follow的官方教程。因为我自己就是跟着官方文档跑的时候报错了,才进行了一些小的修改,主要是...

    生信技能树
  • 地图| 高德地图源码级使用大全

    高德地图提供包括:web前端、Android、iOS、服务器、小程序等平台的地图服务, 地图功能众多,本文记载的只是自己遇到的一些问题,绝大部分功能只要参照官...

    進无尽
  • 地图SDK全面升级 – 数十项新功能及优化等你来体验

    腾讯位置服务地图SDK是一套提供多种地理位置服务的应用程序接口。通过调用该接口,开发者可以在自己的应用中加入地图相关的功能(如地图展示、标注、绘制图形等),轻...

    腾讯位置服务
  • HashMap多线程并发问题分析

    ?

    用户1263954
  • 2019-1-29-多线程UI的多线程问题

    详情可以参见这个大大的博客Launching a WPF Window in a Separate Thread, Part 1 : Reed Copsey, ...

    黄腾霄
  • strong,weak, retain, assign的区别@property的参数

    用户1941540

扫码关注云+社区

领取腾讯云代金券