1.引
这是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);
}
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 (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
粗略地看了一下,就是在getter和setter里添加了一把叫SpinLock的锁,保证了setter和getter的线程安全
2.那么啥是SpinLock呢?
了解一下SpinLock
中文叫自旋锁,当一个线程获得锁之后,其他线程将会一直循环在哪里查看是否该锁被释放。所以,此锁比较适用于锁的持有者保存时间较短的情况下。
在OC里它是OSSpinLock,OS为前缀说明它是MacOS的API
这里有一个问题,为什么苹果使用自旋锁,而不使用其它的锁?
赋值操作是一个非常快速的操作,如果用互斥锁的话,让等待外面的线程休眠,再醒来,是一件多么痛苦的事情啊,好不容易睡着了,睡没几毫秒就得起来,会有起床气的!这里的起床气意思是消耗CPU资源~
3.atomic安全吗?
这是一道经典的面试题!-_-
看如下代码
@property (atomic, assign) NSInteger plus;//有一个atomic的Int数,用来自增
有一个自增的方法,循环10次,为了模拟线程抢夺的场景,我让当前线程睡1秒后执行自增然后打印当前线程以及自增后的值
- (void)plusMethod
{
for (NSInteger i = 0; i < 10; i++) {
sleep(1);
self.plus++;
NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus);
}
}
最后创建两条线程去操作这个自增方法
- (void)gcd
{
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
[self plusMethod];
});
dispatch_async(queue, ^{
[self plusMethod];
});
}
想一下打印的结果,是不是0,1,2...19
打印的结果
{number = 3, name = (null)} plus:2
{number = 4, name = (null)} plus:2
{number = 4, name = (null)} plus:4
{number = 3, name = (null)} plus:3
{number = 4, name = (null)} plus:5
{number = 3, name = (null)} plus:6
{number = 3, name = (null)} plus:8
{number = 4, name = (null)} plus:8
{number = 3, name = (null)} plus:9
{number = 4, name = (null)} plus:10
{number = 4, name = (null)} plus:11
{number = 3, name = (null)} plus:12
{number = 3, name = (null)} plus:13
{number = 4, name = (null)} plus:14
{number = 4, name = (null)} plus:15
{number = 3, name = (null)} plus:15
{number = 4, name = (null)} plus:16
{number = 3, name = (null)} plus:17
{number = 3, name = (null)} plus:18
{number = 4, name = (null)} plus:18
结果的确是打印了20次,但是打印的结果并不是我们想象中的1~20,也就是说 这段 代码由线程安全的问题。
所以我以前就认为是atomic并不是线程安全的,所以推荐使用自己去管理线程安全,使用nonatomic,但真的是这样吗?
把自增的方法改造一下:
添加一把锁, 我也加的是自旋锁,在这个例子里,不考虑性能的情况,加什么锁效果是一样的
- (void)plusMethod
{
for (NSInteger i = 0; i < 10; i++) {
sleep(1);
OSSpinLockLock(&_spinLock);// <----
self.plus++;
NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus);
OSSpinLockUnlock(&_spinLock);// <-----
}
}
打印的结果就不贴了,结果就是我们想要的,1~20,线程安全了!
分析一下:
OSSpinLockLock(&_spinLock);// <----
self.plus++;
OSSpinLockUnlock(&_spinLock);// <-----
这代码只是吧self.plus的外层加了一把锁,不考虑性能的情况下,这是重复操作,不起什么效果的,那么罪魁祸首就是这个
NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus);
我们把每一条指令夸张一点,我们把这个当做试一次厕所愉快的旅行,有A和B两个人吃坏了肚子,有一间厕所,里面只有一个马桶,这个马桶设计的很奇怪,冲水按钮放在门外。
++操作就是一次上大号,需要5秒,NSLog的操作想成是冲水,需要3秒,来也匆匆,去也冲冲嘛~
程序开始,A肚子涡轮开始旋转要上厕所了,发现没人,它就进去把门关了,开始卸货需要耗时5秒;
这时候B涡轮也转起来了也想来上大号啊,一看有人,他很着急啊,他就在门口来回徘徊,等待啊A出来!
A在里面很无聊啊,没有人告诉它一定要拉完才能冲水,它就按了一下马桶的冲水,马桶开始冲水,哗啦啦,A很舒服,并发出了爽朗的笑声;
B很不爽啊,怎么还不出,急死了,它看到了门口有冲水的按钮,于是它也去按了一下冲水;
5秒过去了,A出来了,把门打开;
B进去,把门锁了,开始卸货,也按了一下门口了冲水;
A一出来发现肚子又痛了,着急外面等,也按了一下冲水按钮
如此反复。。。
额。。。所以我们为了防止冲水被破坏,得把这个冲水按钮设计到门里面去,
- (void)plusMethod
{
for (NSInteger i = 0; i < 10; i++) {
sleep(1);
OSSpinLockLock(&_spinLock);// <----
self.plus++;
NSLog(@"%@ plus:%ld", NSThread.currentThread, self.plus);
OSSpinLockUnlock(&_spinLock);// <-----
}
}
这就是为什么说atomic只是保证了读写的安全并不能保证真的线程安全了,在项目中,有很多逻辑是需要同步后才将plus++,所以导致线程不安全!
其实atomic是可以保证数据的安全的!
4.确定atomic是安全的吗?
刚刚说“其实atomic是可以保证数据的安全的!”,现在又问,是不是撒!
苹果的代码是没有问题的,但如果这把锁出了问题呢?YY大大在16年在博客发了一篇帖子 戳这里YY大大的博客 不想点进去的,我截取了关键内容,如下
高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
最终的结论就是,除非开发者能保证访问锁的线程全部都处于同一优先级,否则 iOS 系统中所有类型的自旋锁都不能再使用了。
我在使用OSSpinLock的时候,已经有警告了,推荐使用os_unfair_lock
'OSSpinLock' is deprecated: first deprecated in iOS 10.0 - Use os_unfair_lock() from instead
如果真的要使用你需要引入这个库
#import
5.os_unfair_lock
需要引入这个库
#import
使用API
os_unfair_lock_t unfairLock = &(OS_UNFAIR_LOCK_INIT);// 初始化
os_unfair_lock_lock(unfairLock); // 加锁
os_unfair_lock_unlock(unfairLock); // 解锁
BOOL b = os_unfair_lock_trylock(unfairLock); // 尝试加锁
6.题外话
在Swift里,SpinLock是一个宏,它的值一把递归锁~