《iOS面试之道》读书笔记 - atomic/nonatomic
atomic
和nonatomic
用于在定义 property 时指明其原子性:
-
atomic
表示是原子性的,调用该 property 的 getter 和 setter 会保证对象的完整性。多线程操作时,任何调用都可以得到一个完整的对象,因此速度较慢。 -
nonatomic
表示是非原子性的,调用该 property 的 getter 和 setter 不保证对象的完整性。多个线程对它进行访问,它可能会返回未初始化的对象,因此速度较快。
注意,以上讨论仅对编译器自动生成的 getter/setter 有效。如果你自己实现了 getter/setter,无论使用 atomic/nonatomic,property 的原子性都将取决于你自己的实现。
使用 atomic 修饰的 property 是如何保证原子性的?
对于任何没有手动实现的属性,编译器都会生成一个objc_getProperty_non_gc
的函数作为 getter,同时生成一个objc_setProperty_non_gc
的函数作为 setter。
从objc_getProperty_non_gc
函数的源代码可以看到[1]:
// objc-accessors.mm
id objc_getProperty_non_gc(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);
}
而objc_setProperty_non_gc
函数最终将调到reallySetProperty
函数[2]:
// objc-accessors.mm
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);
}
其中处理 atomic 的分支里,getter/setter 都使用了在PropertyLocks
中的128个自旋锁(Spinlock)中的1个来给操作上锁。这是一种务实和快速的方式,最糟糕的情况下,如果遇到了哈希碰撞,那么 setter 需要等待另一个和它无关的 setter 完成之后再进行工作[3]。
使用 atomic 修饰的 property 是线程安全的吗?
考虑对一个 nonatomic 的整形变量integer
进行加1操作,可分为三个步骤:首先从内存中取出原始值,然后加1,最后将修改后的值写回内存。
现在有A
和B
两个并发线程,分别对integer
进行加1操作,那么问题来了:两个线程都从变量中取出了原始值,假设17
,然后A
将值加1,然后将结果18
写回内存;同时B
也将值加1后将结果18
写回内存。此时integer
被加1了两次,但最终值却是18
。
这个问题称为竞态条件,atomic 通过在读写时加入自旋锁,能保证对象的完整性,保护你免于在 setter 中遭遇到竞态条件的困扰。
但这并不代表使用 atomic 就是线程安全的。考虑三个并发线程A
,B
和C
,其中A
和B
对变量integer
进行加1操作,C
从变量integer
中读取数据。因为三个线程是并发的,所以C
读取数据的时机可能在其他两个写入数据的线程A
、B
之前,或在他们两个之间,也可能在他们两个之后,导致C
读到的数据可能是17
、18
或19
。
如何保证 property 的线程安全性?
没有银弹,具体问题具体分析。@synchronized
和dispatch_queue
都是可选的解决方案,但也都各有利弊。这里就不展开说明了,那将又是一大块篇幅。