irpas技术客

ios 原子属性nonatomic/atomic_想名真难_ios 原子性

未知 3441

原子属性 (Atomic Properties)

你曾经好奇过 Apple 是怎么处理 atomic 的设置/读取属性的么?至今为止,你可能听说过自旋锁 (spinlocks),信号量(semaphores),锁 (locks),@synchronized 等,Apple 用的是什么呢?因为 Objctive-C 的 runtime 是开源的,所以我们可以一探究竟。 在MRC下, 一个非原子的 setter 看起来是这个样子的:

- (void)setUserName:(NSString *)userName { if (userName != _userName) { [userName retain/copy]; // 根据属性的内存管理语义 [_userName release]; _userName = userName; } }

这是一个MRC下的retain/release 的版本,ARC 生成的代码和这个看起来也是类似的。当我们看这段代码时,显而易见要是 setUserName: 被并发调用的话会造成麻烦。我们可能会释放 _userName 两次,这回使内存错误,并且导致难以发现的 bug。 对于任何没有手动实现的属性,编译器都会生成一个 objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 的调用。在我们的例子中,这个调用的参数是这样的:

一共6个参数, 下面有具体实现

objc_setProperty_non_gc(self, _cmd, (ptrdiff_t)(&_userName) - (ptrdiff_t)(self), userName, NO, NO);

ptrdiff_t 可能会吓到你,但是实际上这就是一个简单的指针算术,因为其实 Objective-C 的类仅仅只是 C 结构体而已。 objc_setProperty 调用的是如下方法:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { if (offset == 0) { object_setClass(self, newValue); return; } id oldValue; id *slot = (id*) ((char*)self + offset); if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); } if (!atomic) { oldValue = *slot; *slot = newValue; } else { spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); } objc_release(oldValue); }

其实方法实际做的事情非常直接,

非原子性, 用一个临时变量存放旧值,用新值给这个内存区域赋值释放旧值原子性的情况 先用旧值作为key, 从字典中取出一把锁加锁用一个临时变量存放旧值,用新值给这个内存区域赋值解锁释放旧值

加锁使用了?PropertyLocks 中的自旋锁spinlock_t中的 1 个来给操作上锁。这是一种务实和快速的方式, set/get本身是一个非常轻量级的操作, 忙等待就行了.

我当时的runtime版本是objc4-750, 看到这个自旋锁之后还在想这个不是已经不推荐了吗, 怎么苹果自己还在用, 又跑到官网上看了一个最新的objc4-818.2确认了一下, 最新的818.2也是使用的自旋锁, 看来苹果对这个atomic不怎么上心了.??Runtime官方开源地址

PropertyLocks是一个hashMap,类似于字典,内部使用一个固定长度的数组存放锁,runtime初始化的时候就会创建完成锁并放入到数组中, 在iOS真机上数组数量固定为8,其他设备为64, 也就是说, iOS的PropertyLocks最多提供8个自旋锁给属性的atomic使用.

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { if (offset == 0) { return object_getClass(self); } // Retain release world id *slot = (id*) ((char*)self + offset); if (!atomic) return *slot; // Atomic retain release world spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock(); // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock. return objc_autoreleaseReturnValue(value); }

get方法也很简单,通过偏移量取出对应地址的值,

如果是nonatomic的话, 直接返回, 结束如果是atomic的话, 会先从一个字典中取锁, 以返回值作为key取出对应的锁,用取出的锁进行加锁,对返回值进行一次retain解锁把返回值加入自动释放池, 返回,?

通过set/get源码的阅读, 我们也可以理解为什么苹果不推荐使用atomic了.

PropertyLocks是一个全局生效的字典, 最多提供8个自旋锁给atomic加锁解锁, 当一个项目中有几万个属性都是原子性的时候, 很多属性都会对应到同一把锁上, 那么这个属性就得等其他毫不相关的属性完成读写, 自己才能进行操作, 而且set/get是一个特别高频的操作.PropertyLocks使用的是自旋锁, 自旋锁的特点是忙等待, 当有几万的属性对应到8把锁上, 忙等待就是一个非常常见的操作, 忙等待对cpu的消耗很大, 手机发烫, 效率变低,?而且这样的忙等待是无意义的.atomic只能针对特定场景保证线程安全, 存在局限性. 只能保证set/get的线程安全, 对于更大范围的线程安全是无法保证的.? ? ? ? ??举一个很简单的例子,???假设定义属性 NSInteger i 是原子的,对i进行 i = i + 1; 这个操作就是不安全的。因为原子性只能保证读写安全,而该表达式需要三步操作: 1.先进行get, 读取i的值存入寄存器; 2.将寄存器的值加1; 3.使用寄存器修改后的值给i赋值; atomic只能保证1和3是线程安全的, 如果在第1步完成的时候,i被其他线程修改了,那么表达式执行的结果就会与预期的不一样,也就是不安全的。所以要解决这样的线程安全问题, 只能对 整个表达式进行加锁, 单纯对i 设置atomic达不到预期的.

综上3点, atomic在大范围使用时效率低下, 而且效果不太好, 存在局限性, 这可能就是苹果不推荐atomic的原因了.

虽然这些方法没有定义在任何公开的头文件中,但我们还是可用手动调用他们。我不是说这是一个好的做法,但是知道这个还是蛮有趣的,而且如果你想要同时实现原子属性和自定义的 setter 的话,这个技巧就非常有用了。

// 手动声明运行时的方法 extern void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, BOOL shouldCopy); extern id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic); #define PSTAtomicRetainedSet(dest, src) objc_setProperty(self, _cmd, (ptrdiff_t)(&dest) - (ptrdiff_t)(self), src, YES, NO) #define PSTAtomicAutoreleasedGet(src) objc_getProperty(self, _cmd, (ptrdiff_t)(&src) - (ptrdiff_t)(self), YES)

为何不用 @synchronized ?

你也许会想问为什么苹果不用 @synchronized(self) 这样一个已经存在的运行时特性来锁定属性?? 主要原因还是效率问题.

你可以看看苹果的源代码,就会发现其实发生了很多的事情。Apple 使用了最多三个加/解锁序列,还有一部分原因是他们也添加了异常开解(exception unwinding)机制。相比于更快的自旋锁方式,@synchronized实现要慢得多。由于设置某个属性一般来说会相当快,因此自旋锁更适合用来完成这项工作。@synchonized(self) 更适合使用在你需要确保在发生错误时代码不会产生死锁,而是抛出异常的时候。

多线程下出错案例分析

if (self.contents) { ? ? CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,? ? ? ? (__bridge CFStringRef)self.contents, NULL); ? ? // 渲染字符串 }

多线程下存在contents属性在通过检查之后却又被设成了nil而导致EXC_BAD_ACCESS崩溃。捕获这个变量就可以简单修复这个问题。

NSString *contents = self.contents; if (contents) { ? ? CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,? ? ? ? (__bridge CFStringRef)contents, NULL); ? ? // 渲染字符串 }

类似的问题也可能出现在block中, 但是这时候使用局部变量接收不能解决问题, 还是需要通过传统的加锁/解锁来处理, 明明判断block有值才执行,为什么还是crash

数组,字典尽量用不可变的版本,没有多线程并发的问题, 如果需要添加/删除操作,可以使用局部变量作为可变版本,可变版本的修改完成后进行copy. 注意:对不可变的属性进行赋值的操作也要保证线程安全

// 方案1:使用atomic,下面的方法就没有必要加锁了, // 方案2:此处用nonatomic,在set/get的地方加锁, // 一开始尝试了只在set加锁,get不加锁,发现不可以,必须set/get都加锁才能线程安全 @property (nonatomic, strong) NSArray *dataArray; - (void)addDelegate:(id<NSObject>)delegate { ? ? @synchronized(self) { ? ? ? ? NSMutableArray *tempArray = [NSMutableArray arrayWithArray:self.dataArray]; ? ? ? ? [tempArray addObject:delegate]; ? ? ? ? self.dataArray = [tempArray copy]; ? ? } } - (void)removeDelegate:(id<NSObject>)delegate { ? ? @synchronized(self) { ? ? ? ? NSMutableArray *tempArray = [NSMutableArray arrayWithArray:self.dataArray]; ? ? ? ? [tempArray removeObject:delegate]; ? ? ? ? self.dataArray = [tempArray copy]; ? ? } } - (void)removeAllDelegates { ? ? @synchronized(self) { ? ? ? ? self.dataArray = nil; ? ? } } - (void)callDelegate { ?? ?NSArray *array = nil; ? ? @synchronized(self) { ?? ??? ?array= [NSArray arrayWithArray:self.dataArray]; ? ? } ? ? [array enumerateObjectsUsingBlock:^(id<NSObject> delegate, NSUInteger idx, BOOL *stop) { ? ? ? ? // 调用delegate ? ? }]; }

参考文章:?iOS 记住这些方法,轻松设计自己的线程安全类


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #iOS #原子性 #原子属性 #atomic #apple #是怎么处理