本文首发于 个人博客
多线程中的锁通常分为互斥锁和自旋锁,这篇文章主要向大家介绍一些自旋锁的原理以及atomic的底层实现。
自旋锁
⚛维基百科上对自旋锁的解释:
自旋锁 是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种
忙等
(忙碌等待)。一旦获取了自旋锁,线程会一直持有该锁,直至显式释放自旋锁。获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的
(atomic)
。通常用text-and-set
原子操作来实现。
自旋锁
的核心就是忙等,尝试自定义一个自旋锁如下:
struct spinlock {
int flag;
};
@implementation TPSpinLock {
spinlock _lock;
}
- (instancetype)init {
self = [super init];
if (self) {
_lock = spinlock{0};
}
return self;
}
- (void)lock {
while (test_and_set(&_lock.flag, 1)) {
// wait
}
}
- (void)unlock {
_lock.flag = 0;
}
int test_and_set(int *old_ptr, int _new) {
int old = *old_ptr;
*old_ptr = _new;
return old;
}
@end
如上述代码,我们自定义了test_and_set
方法,当线程1
进行lock
操作的时候会传入flag = 0
,test_and_set
方法返回0
的同时并将flag = 1
,这个时候线程2
执行lock
的时候一直返回1
,那么就一直执行while(1)
处于等待状态,直到线程1
执行unlock
将flag = 0
这个时候就打破while
循环,线程2
就能继续执行并加锁。
atomic
说起自旋锁,无不联想到属性的原子操作,即 atomic
-
atomic
底层是如何实现的? -
atomic
绝对安全吗?
带着这些问题我们对 atomic
进行探讨,我们来到 objc源码 处进行查看,atomic
既然是修饰property
的,那么必然会跟property
的set
和get
方法相关,我们找到了相关方法的实现:
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);
}
set
方法atomic
那块加了判断,如果是原子性就会进行加锁和解锁操作。
再看 get
方法:
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);
}
很明显,也是对原子操作进行加锁处理。
我们注意到所有的源码中针对加锁的地方都是定义为spinlock
,也就是自旋锁,所以通常被人问到我们atomic
底层是什么的时候,我们都回答 自旋锁
,结合YY大神的不再安全的OSSpinLock 一文,可以看出Apple
已经弃用OSSpinLock
了,内部确如下述代码那样是用os_unfair_lock
来实现的,探其底层执行lock
和unlock
的其实是mutex_t
,也就是互斥锁
。
// property的set方法
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
// atomic中用到的锁
using spinlock_t = mutex_tt<LOCKDEBUG>;
// mutex_tt 的结构
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
public:
constexpr mutex_tt() : mLock(OS_UNFAIR_LOCK_INIT) {
lockdebug_remember_mutex(this);
}
constexpr mutex_tt(const fork_unsafe_lock_t unsafe) : mLock(OS_UNFAIR_LOCK_INIT) { }
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);
}
// 上述lock方法的实现
void
lockdebug_mutex_lock(mutex_t *lock)
{
auto& locks = ownedLocks();
if (hasLock(locks, lock, MUTEX)) {
_objc_fatal("deadlock: relocking mutex");
}
setLock(locks, lock, MUTEX);
}
所以说 atomic
的本质并不是自旋锁,至少当前不是,我查询了 objc
之前的源码发现老版本的 atomic
的实现,确实不一样:
typedef uintptr_t spin_lock_t;
OBJC_EXTERN void _spin_lock(spin_lock_t *lockp);
OBJC_EXTERN int _spin_lock_try(spin_lock_t *lockp);
OBJC_EXTERN void _spin_unlock(spin_lock_t *lockp);
由此可知:
atomic
原子操作只是对setter
和getter
方法进行加锁
那么第二个问题来了:atomic
绝对安全吗?我们接着分析,首先看下面的代码,最终的 number
会是多少?20000
?
@property (atomic, assign) NSInteger number;
- (void)atomicTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10000; i ++) {
self.number = self.number + 1;
NSLog(@"A-self.number is %ld",self.number);
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10000; i ++) {
self.number = self.number + 1;
NSLog(@"B-self.number is %ld",self.number);
}
});
}
打印结果:
NO
并不是 20000
,这是为啥呢?我们的 number
是 atomic
进行加锁了啊,为什么还会出现线程安全问题。其实答案上文已经有了,只是需要我们仔细去品,atomic
只是针对 setter
和 getter
方法进行加锁,上述代码有两个异步线程同时执行,如果某个时间 A线程
执行到getter
方法,之后 cpu
立即切换到 线程B
去执行他的get方法那么这个时候他们进行 +1
的处理并执行setter
方法,那么两个线程的 number
就会是一样的结果,这样我们的 +1
就会出现线程安全问题,就会导致我们的数字出现偏差,那么我们找一找打印数字里是否有重复的:
功夫不负有心人,我们果然找到了重复的,那么基于我们 20000
的循环次数少个百八十的太正常不过了。
总结
-
自旋锁
不同于互斥锁
如果访问的资源被占用,它会处于忙等
状态。自旋锁由于一直处于忙等状态所以他在线程锁被释放的时候会立即获取而不用唤醒,所以其执行效率是很高的,尤其是在多核的cpu上运行效率很高,但是其忙等的状态会消耗cpu
的性能,所以其性能比互斥锁要低很多。 -
atomic
的底层实现,老版本是自旋锁
,新版本是互斥锁
。 -
atomic
并不是绝对线程安全,它能保证代码进入getter
和setter
方法的时候是安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了getter
和setter
方法,其线程安全就要由程序员自己来把握,所以atomic
属性和线程安全并没有必然联系。