iOS property的多线程问题解析

时间:2022-07-23
本文章向大家介绍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)导致的事故