iOS-atomic修饰符原理剖析讲解 (你将会了解到什么是优先级翻转、自旋锁、互斥锁)
前言
这里面你将会了解到什么是优先级翻转、自旋锁、互斥锁。
绝大部分 Objective-C 程序员使用属性时,都不太关注一个特殊的修饰前缀,一般都无脑的使用其非默认缺省的状态,他就是 atomic。
1 @interface PropertyClass 2 3 @property (atomic, strong) NSObject *atomicObj; //缺省也是atomic 4 @property (nonatomic, strong) NSObject *nonatomicObj; 5 6 @end
入门教程中一般都建议使用非原子操作,因为新手大部分操作都在主线程,用不到线程安全的特性,大量使用还会降低执行效率。
那他到底怎么实现线程安全的呢?使用了哪种技术呢?
原理
属性的实现
首先我们研究一下属性包含的内容。通过查阅源码,其结构如下:
1 struct property_t { 2 const char *name; //名字 3 const char *attributes; //特性 4 };
属性的结构比较简单,包含了固定的名字和元素,可以通过 property_getName 获取属性名,property_getAttributes 获取特性。
上例中 atomicObj 的特性为 T@"NSObject",&,V_atomicObj,其中 V 代表了 strong,atomic 特性缺省没有显示,如果是 nonatomic 则显示 N。
那到底是怎么实现原子操作的呢? 通过引入runtime,我们能调试一下调用的函数栈。
可以看到在编译时就把属性特性考虑进去了,Setter 方法直接调用了 objc_setProperty 的 atomic 版本。这里不用 runtime 去动态分析特性,应该是对执行性能的考虑。
1 static inline void reallySetProperty(id self, SEL _cmd, 2 id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) { 3 //偏移为0说明改的是isa 4 if (offset == 0) { 5 object_setClass(self, newValue); 6 return; 7 } 8 9 id oldValue; 10 id *slot = (id*) ((char*)self + offset);//获取原值 11 //根据特性拷贝 12 if (copy) { 13 newValue = [newValue copyWithZone:nil]; 14 } else if (mutableCopy) { 15 newValue = [newValue mutableCopyWithZone:nil]; 16 } else { 17 if (*slot == newValue) return; 18 newValue = objc_retain(newValue); 19 } 20 //判断原子性 21 if (!atomic) { 22 //非原子直接赋值 23 oldValue = *slot; 24 *slot = newValue; 25 } else { 26 //原子操作使用自旋锁 27 spinlock_t& slotlock = PropertyLocks[slot]; 28 slotlock.lock(); 29 oldValue = *slot; 30 *slot = newValue; 31 slotlock.unlock(); 32 } 33 34 objc_release(oldValue); 35 } 36 37 id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { 38 // 取isa 39 if (offset == 0) { 40 return object_getClass(self); 41 } 42 43 // 非原子操作直接返回 44 id *slot = (id*) ((char*)self + offset); 45 if (!atomic) return *slot; 46 // 原子操作自旋锁 47 spinlock_t& slotlock = PropertyLocks[slot]; 48 slotlock.lock(); 49 id value = objc_retain(*slot); 50 slotlock.unlock(); 51 // 出于性能考虑,在锁之外autorelease 52 return objc_autoreleaseReturnValue(value); 53 }
什么是自旋锁呢?
锁用于解决线程争夺资源的问题,一般分为两种,自旋锁(spin)和互斥锁(mutex)。
互斥锁可以解释为线程获取锁,发现锁被占用,就向系统申请锁空闲时唤醒他并立刻休眠。互斥锁加锁的时候,等待锁的线程处于休眠状态,不会占用CPU的资源
自旋锁比较简单,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。自旋锁加锁的时候,等待锁的线程处于忙等状态,并且占用着CPU的资源。
原子操作的颗粒度最小,只限于读写,对于性能的要求很高,如果使用了互斥锁势必在切换线程上耗费大量资源。相比之下,由于读写操作耗时比较小,能够在一个时间片内完成,自旋更适合这个场景。
自旋锁的坑
但是iOS 10之后,苹果因为一个巨大的缺陷弃用了 OSSpinLock 改为新的 os_unfair_lock。
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。描述引用自 ibireme 大神的文章。
优先级翻转的问题
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。导致陷入死锁。
这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。iOS10以后,苹果给出了新的api
那为什么原子操作用的还是 spinlock_t 呢?
using spinlock_t = mutex_tt<LOCKDEBUG>; using mutex_t = mutex_tt<LOCKDEBUG>; class mutex_tt : nocopy_t { os_unfair_lock mLock; //处理了优先级的互斥锁 void lock() { lockdebug_mutex_lock(this); os_unfair_lock_lock_with_options_inline (&mLock, OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION); } void unlock() { lockdebug_mutex_unlock(this); os_unfair_lock_unlock_inline(&mLock); } }
差点被苹果骗了!原来系统中自旋锁已经全部改为互斥锁实现了,只是名称一直没有更改。
为了修复优先级反转的问题,苹果也只能放弃使用自旋锁,改用优化了性能的 os_unfair_lock,实际测试两者的效率差不多。
os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持
从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
问答
atomic的实现机制
使用atomic 修饰属性,编译器会设置默认读写方法为原子读写,并使用互斥锁添加保护。
为什么不能保证绝对的线程安全?
单独的原子操作绝对是线程安全的,但是组合一起的操作就不能保证。
1 - (void)competition { 2 self.intSource = 0; 3 4 dispatch_async(queue1, ^{ 5 for (int i = 0; i < 10000; i++) { 6 self.intSource = self.intSource + 1; 7 } 8 }); 9 10 dispatch_async(queue2, ^{ 11 for (int i = 0; i < 10000; i++) { 12 self.intSource = self.intSource + 1; 13 } 14 }); 15 }
最终得到的结果肯定小于20000。当获取值的时候都是原子线程安全操作,比如两个线程依序获取了当前值 0,于是分别增量后变为了 1,所以两个队列依序写入值都是 1,所以不是线程安全的。
解决的办法应该是增加颗粒度,将读写两个操作合并为一个原子操作,从而解决写入过期数据的问题。
1 os_unfair_lock_t unfairLock; 2 - (void)competition { 3 self.intSource = 0; 4 5 unfairLock = &(OS_UNFAIR_LOCK_INIT); 6 dispatch_async(queue1, ^{ 7 for (int i = 0; i < 10000; i++) { 8 os_unfair_lock_lock(unfairLock); 9 self.intSource = self.intSource + 1; 10 os_unfair_lock_unlock(unfairLock); 11 } 12 }); 13 14 dispatch_async(queue2, ^{ 15 for (int i = 0; i < 10000; i++) { 16 os_unfair_lock_lock(unfairLock); 17 self.intSource = self.intSource + 1; 18 os_unfair_lock_unlock(unfairLock); 19 } 20 }); 21 }
总结
通过学习属性的原子性,对系统中锁的理解又加深,包括自旋锁,互斥锁,读写锁等。
本来都以为实现是自旋锁了,还好留了个心眼多看了一层才发现最终实现还是互斥锁。这件事也给我一个小教训,查阅源码还是要刨根问底,只浮于表面的话,可能得不到想要的真相。