atomic 线程安全吗?

自旋锁

⚛维基百科上对自旋锁的解释:

自旋锁 是计算机科学用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种 忙等 (忙碌等待)。一旦获取了自旋锁,线程会一直持有该锁,直至显式释放自旋锁。

获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的(atomic)。通常用 text-and-set 原子操作来实现。

自旋锁的核心就是忙等,尝试自定义一个自旋锁如下:

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
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 = 0test_and_set方法返回0的同时并将flag = 1,这个时候线程2 执行lock的时候一直返回1,那么就一直执行while(1)处于等待状态,直到线程1执行unlockflag = 0 这个时候就打破while循环,线程2就能继续执行并加锁。

atomic

说起自旋锁,无不联想到属性的原子操作,即 atomic

  • atomic 底层是如何实现的?
  • atomic 绝对安全吗?

带着这些问题我们对 atomic 进行探讨,我们来到 objc源码 处进行查看,atomic 既然是修饰property的,那么必然会跟propertysetget方法相关,我们找到了相关方法的实现:

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
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 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 来实现的,探其底层执行lockunlock的其实是mutex_t,也就是互斥锁

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
// 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 的实现,确实不一样:

1
2
3
4
5
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 原子操作只是对settergetter 方法进行加锁

那么第二个问题来了:atomic绝对安全吗?我们接着分析,首先看下面的代码,最终的 number 会是多少?20000?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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,这是为啥呢?我们的 numberatomic 进行加锁了啊,为什么还会出现线程安全问题。其实答案上文已经有了,只是需要我们仔细去品,atomic 只是针对 settergetter 方法进行加锁,上述代码有两个异步线程同时执行,如果某个时间 A线程 执行到getter方法,之后 cpu 立即切换到 线程B 去执行他的get方法那么这个时候他们进行 +1 的处理并执行setter方法,那么两个线程的 number 就会是一样的结果,这样我们的 +1就会出现线程安全问题,就会导致我们的数字出现偏差,那么我们找一找打印数字里是否有重复的:

功夫不负有心人,我们果然找到了重复的,那么基于我们 20000 的循环次数少个百八十的太正常不过了。

总结

  • 自旋锁 不同于互斥锁 如果访问的资源被占用,它会处于 忙等 状态。自旋锁由于一直处于忙等状态所以他在线程锁被释放的时候会立即获取而不用唤醒,所以其执行效率是很高的,尤其是在多核的cpu上运行效率很高,但是其忙等的状态会消耗cpu的性能,所以其性能比互斥锁要低很多。
  • atomic 的底层实现,老版本是自旋锁,新版本是互斥锁
  • atomic 并不是绝对线程安全,它能保证代码进入 gettersetter 方法的时候是安全的,但是并不能保证多线程的访问情况下是安全的,一旦出了 gettersetter 方法,其线程安全就要由程序员自己来把握,所以 atomic 属性和线程安全并没有必然联系。