源码版本:objc4-781,objc-accessors.mm文件中。
原子操作是指操作是不可分割的,要么发生,要么不发生。事物只会处于原始状态和成功状态两种中的一种,不会处于一种半完成状态。
OC 中的atomic修饰符 源码实现:
getter 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 id objc_getProperty (id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { if (offset == 0 ) { return object_getClass (self); } id *slot = (id*) ((char *)self + offset); if (!atomic) return *slot; spinlock_t & slotlock = PropertyLocks[slot]; slotlock.lock (); id value = objc_retain (*slot); slotlock.unlock (); return objc_autoreleaseReturnValue (value); }
对于none atomic属性,getter方法只是简单的返回对象地址。 对于atomic属性,getter方法会加锁进行retain操作,并返回一个注册到自动释放池里的对象地址。这样就可以防止getter的对象不会因为其他线程的setter操作导致销毁。
setter 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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); } void objc_setProperty_atomic (id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty (self, _cmd, newValue, offset, true , false , false ); } void objc_setProperty_nonatomic (id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty (self, _cmd, newValue, offset, false , false , false ); } void objc_setProperty_atomic_copy (id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty (self, _cmd, newValue, offset, true , true , false ); } void objc_setProperty_nonatomic_copy (id self, SEL _cmd, id newValue, ptrdiff_t offset) { reallySetProperty (self, _cmd, newValue, offset, false , true , false ); }
上述锁从何而来:从objc-accessors.mm中定义的全局PropertyLocks哈希表中获取。
1 2 3 StripedMap<spinlock_t> PropertyLocks StripedMap<spinlock_t> StructLocks StripedMap<spinlock_t> CppObjectLocks
可以看到使用atomic修饰的属性,编译器在合成getter/setter方法时,相比于nonatomic的属性会有额外的处理:
加锁(自旋锁),保证原子性。其他线程需要等待。
getter方法获取到对象后会先retain再注册到自动释放池里最后返回给调用者,确保调用者获取到的对象生命周期不受后续setter影响。
atomic保证了赋值/取值的整个过程的完整性,并且不受其他线程的干扰 。比如线程A在setter时,线程B如果想setter那么就需要等待,线程C想getter也需要等待。
再来看一下使用nonatomic在多线程环境下,下面的场景会出现什么问题:
1、A,B线程同时调用setter方法会怎样。
1 2 3 4 5 6 7 8 9 10 11 12 if (!atomic) {1 oldValue = *slot; 2 *slot = newValue; } else { spinlock_t & slotlock = PropertyLocks[slot]; slotlock.lock (); oldValue = *slot; *slot = newValue; slotlock.unlock (); } 3 objc_release (oldValue);
A,B同时执行1,因此获得了同一个旧值对象,然后各自执行2因此属性的值是不确定的(这里倒没有什么致命关系),最后执行3,于是一个对象被释放了两次,这有可能会导致野指针崩溃。而atomic有加锁保护所以不会有问题。
2、线程A调用getter方法获得对象地址,在对象使用期间,线程B调用setter方法,会怎样?
getter方法实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 id objc_getProperty (id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { if (offset == 0 ) { return object_getClass(self); } id *slot = (id*) ((char *)self + offset); if (!atomic) return *slot; spinlock_t & slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock(); return objc_autoreleaseReturnValue(value); }
对于nonatomic,getter方法获取到旧值后只是简单返回。上述情况nonatomic可能会因为线程B调用setter方法释放旧值而造成线程A潜在的野指针访问崩溃风险。而atomic会先retain再注册到自动释放池里最后返回给调用者,因此不会存在这种风险。
iOS12.5.4 iPhone6plus
例1:
例2:
这些都是野指针崩溃,访问了已经销毁的对象。
因此如果属性存在多线程访问的情况,应该使用atomic并使用存取器访问实例变量,如果有需要还应该配合锁使用。
注意:atomic无法保证对象自身的线程安全。
官方文档对atomic无法保证对象的线程安全举的例子:
考虑XYZPerson,拥有firstName和lastName两个atomic属性。如果一个线程A正在修改这两个属性,与此同时另一个线程B正在读取这两个属性,那么线程B得到的firstName和lastName的值将会发生数据不匹配的情况。这虽然不会导致崩溃问题,但结果是不符合预期的。
eg:
初始时, firstName = @”李”; lastName = @”四”;
线程A想修改为:firstName = @”张”; lastName = @”三”;
与此同时线程B在读取这两个属性,那么线程B有可能得到:
张四,李三。
要想解决的话就应该把firstName = @"张"; lastName = @"三";这一段代码也加锁。
另外一个例子:经典的i++问题。
i++对应的汇编:
1 2 3 0x100368d83 <+19 >: movl -0 x 14 (%rbp ), %eax 0x100368d86 <+22 >: addl $0x1 , %eax 0x100368d89 <+25 >: movl %eax , -0 x 14 (%rbp )
可以看到,完成一次i++总共分为三步:
从内存中取出i的值存放到寄存器上
对寄存器的值+1
将寄存器中的值存放回i的内存
由于需要三条指令,因此在多线程中是不安全的。比如线程A在执行第二步时,线程B刚好在执行第一步,此时线程 B 获取到的将是旧的值,最终i的值将只加了一次。
再来看一下OC 中self.i++;的汇编,i 为 atomic 属性。
源码:
1 2 3 - (void )test_atomic_xiushi { self .i++; }
对应的汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 multithreadDemo`-[XQLockViewController test_atomic_xiushi]: 0x101c90d60 <+0>: pushq %rbp 0x101c90d61 <+1>: movq %rsp, %rbp 0x101c90d64 <+4>: subq $0x20, %rsp 0x101c90d68 <+8>: movq %rdi, -0x8(%rbp) 0x101c90d6c <+12>: movq %rsi, -0x10(%rbp) 0x101c90d70 <+16>: movq -0x8(%rbp), %rax 0x101c90d74 <+20>: movq 0x155f5(%rip), %rsi ; "'i'" 0x101c90d7b <+27>: movq %rax, %rcx 0x101c90d7e <+30>: movq %rcx, %rdi 0x101c90d81 <+33>: movq %rax, -0x18(%rbp) 0x101c90d85 <+37>: callq *0xe2b5(%rip) ; (void *)0x00000001025ec940: objc_msgSend #调用objc_msgSend(self, "i") 0x101c90d8b <+43>: addq $0x1, %rax #将寄存器中的值加 1 0x101c90d91 <+49>: movq 0x156c8(%rip), %rsi ; "setI:" 0x101c90d98 <+56>: movq -0x18(%rbp), %rcx 0x101c90d9c <+60>: movq %rcx, %rdi 0x101c90d9f <+63>: movq %rax, %rdx -> 0x101c90da2 <+66>: callq *0xe298(%rip) ; (void *)0x00000001025ec940: objc_msgSend #调用objc_msgSend(self, "setI:", %rdx) 0x101c90da8 <+72>: addq $0x20, %rsp 0x101c90dac <+76>: popq %rbp 0x101c90dad <+77>: retq
步骤:
1 2 3 4 5 1 .调用objc_msgSend(self , "i" ); 2 .执行addq $0x1 , %rax 3 .调用objc_msgSend(self , "setI:" , %rdx);
跟上面的普通 i++其实是一样的,只不过这里 1,3 执行到 getter/setter 时都会加锁,但问题还是出在第 2 步。当线程 A 正在执行第2步还没来得及执行第 3 步将值写入内存时,线程 B 是可以执行第 1 步的,此时线程 B 将获取到旧的值。于是最终只加了一次。要想线程安全必须将 1,2,3都包在锁内 。
即:
因此即使是atomic的属性也是不能保证上述代码是线程安全的。
总结 atomic只是保证了设值/取值的过程是完整的,不受其他线程的干扰。它无法保证其他使用逻辑是线程安全的 。
比如上面的i++操作。
场景: 使用atomic修饰属性,如果有A、B和C三个线程。其中A和B线程同时对一个属性进行赋值操作,C线程进行取值操作,那么可以保证C线程一定可以取到一个完整的值,但是这个值的内容可能是A线程赋的值,也可能是B线程赋的值,也可能是原始值,虽然取得了完整的值,但是这个值不一定是程序员想要的,所以说atomic并不是线程安全的,它只是保证了属性的setter和getter方法内部是线程安全的。如果你想要真正保证线程安全,那么需要在赋值操作的前后进行加锁和解锁操作,还有注意使用同一把锁。
有没有仅使用atomic就能够满足要求的场景?我想象不到,如果你只需要属性存取线程安全,或许仅使用atomic就可以了。比如上面使用atomic就可以避免多线程下的野指针问题。因此atomic的使用场景有限。
参考 多线程-线程安全
Properties Are Atomic by Default 官方文档
OC 基础之atomic和nonatomic关键字