iOS property的多线程问题解析

作者: 落影loyinglin | 来源:发表于2020-08-23 09:11 被阅读0次

正文

问题列表

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)导致的事故

相关文章

  • iOS property的多线程问题解析

    正文 问题列表 1、以下这段代码,在主线程执行会输出什么? 2、稍作修改,以下代码在主线程执行会输出什么? 3、换...

  • iOS property的多线程问题解析

    正文 问题列表 1、以下这段代码,在主线程执行会输出什么? 2、稍作修改,以下代码在主线程执行会输出什么? 3、换...

  • iOS-多线程相关

    本篇涵盖多线程解析、应用等. 1.iOS多线程--彻底学会多线程之『RunLoop』2.iOS多线程--彻底学会多...

  • iOS 多线程技术最牛图解

    了解更多 iOS 多线程技术,点击阅读以下文章 iOS 多线程技术深度解析 最浅显易懂的iOS多线程技术 - GC...

  • iOS property解析

    原文链接:https://juejin.im/post/5c105c7ce51d4562d138086f

  • iOS 多线程详解

    iOS 多线程详解 Slogan : 可能是最通俗易懂的 iOS多线程 详细解析文章 1. 基础概念 1.1 进程...

  • iOS 多线程

    参考资料:iOS多线程iOS GCD 多线程问题在iOS中目前有4套多线程方案,他们分别是: PthreadsNS...

  • iOS面试总结(初级)

    01:iOS推送机制,实现原理02:多线程编程,GCD,锁,同步异步队列03:property 各种关键字,以及...

  • 重识iOS之Property

    重识iOS之Property 重识iOS之Property

  • 关于@property特性的介绍

    写个小文章简单阐述iOS开发中@property的特性问题。 @Property是声明属性的语法,它可以快速方便的...

网友评论

    本文标题:iOS property的多线程问题解析

    本文链接:https://www.haomeiwen.com/subject/uqtcghtx.html