多线程访问同一个对象,经常会出现意料之外的结果。
这里就从atomic与nonatomic讲起。
atomic
atomic能从一定程度上保证线程安全,但是大部分的情况下并没不能完全保证线程安全。
首先我们看看如果将一个属性设置为atomic的时候,编译器帮我们做了什么?
- 生成原子操作的getter和setter方法
当线程A执行setter方法时,线程B如果需要执行getter方法必须等线程ASetter方法结束后才能执行。
atomic内部实现:
// setter
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
// ...
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// ...
}
// getter
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
// ...
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// ...
}
对于没法在CPU一次读写操作中完成的setter和getter方法,就需要考虑其在多线程中的安全性,否则可能导致crash。
系统是通过自旋锁的方式来保证CPU多次读写的执行顺序。
比如32位的系统中:
一个Bool值占1字节,可以在一次读写操作中完成赋值或取值,因此是线程安全的。
一个指针占4字节,也可以在一次读写操作中完成赋值或取值,因此是线程安全的。
一个double占8字节,则需要两次读写操作才能完成赋值或取值,因此就会存在一个写操作(需两次写操作才能完成),之后就是读操作的可能,导致异常值的现象。
- 设置Memory Barrier
对于Objective C的实现来说,几乎所有的加锁操作最后都会设置memory barrier,atomic本质上是对getter,setter加了锁,所以也会设置memory barrier。
memory barrier能够保证内存操作的顺序,按照我们代码的书写顺序来。听起来有点不可思议,事实是编译器会对我们的代码做优化,在它认为合理的场景改变我们代码最终翻译成的机器指令顺序。也就是说如下代码:
self.intA = 0; //line 1
self.intB = 1; //line 2
编译器可能在一些场景下先执行line2,再执行line1,因为它认为A和B之间并不存在依赖关系,虽然在代码执行的时候,在另一个线程intA和intB存在某种依赖,必须要求line1先于line2执行。
如果设置property为atomic,也就是设置了memory barrier之后,就能够保证line1的执行一定是先于line2的,当然这种场景非常罕见,一则是出现变量跨线程访问依赖,二是遇上编译器的优化,两个条件缺一不可。这种极端的场景下,atomic确实可以让我们的代码更加多线程安全一点,但我写iOS代码至今,还未遇到过这种场景,较大的可能性是编译器已经足够聪明,在我们需要的地方设置memory barrier了。
nonatomic
大部分情况下,我们会选择nonatomic作为属性的attribute,因为它能有效避免线程锁带来的性能消耗,而大部分的多线程安全问题也需要我们自己来完成线程安全。
参考链接:
http://mrpeak.cn/blog/ios-thread-safety/