正文
问题列表
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)导致的事故
网友评论