atomic的实现机制

Posted by CoderLeonidas on December 12, 2018

atomic的实现机制

atomicnonatomic的主要区别就是系统自动生成的getter/setter方法不一样:

  • atomic:系统自动生成的getter/setter方法会进行加锁操作
  • nonatomic:系统自动生成的getter/setter方法不会进行加锁操作

简述

  • atomic是默认关键字,如果写@property时没有指定,那就默认为atomic
  • atomic 会为getter/setter方法加锁,锁是自旋锁

自旋锁是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。

  • 由于加锁,所以atomic的读写会比较消耗性能,因此在iOS开发里不常用,而在macOS中则没有这样的性能瓶颈

  • iOS 10之后,苹果因为一个巨大的缺陷弃用了 OSSpinLock 改为新的 os_unfair_lock

    The iOS scheduler maintains several different priority levels / QoS classes: background, utility, default, user-initiated, user-interactive.

    If any thread in a higher class is runnable then it will always run before every thread in lower classes.

    A thread’s priority will never decay down into a lower class. —摘自《Thread safety of weak properties》

    新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

    当低优先级线程获取了锁,高优先级线程访问时陷入忙等状态,由于是循环调用,所以占用了系统调度资源,导致低优先级线程迟迟不能处理资源并释放锁,导致陷入死锁。这也就是所谓的”优先级反转

实现机制

使用atomic 修饰属性,编译器会设置默认读写方法为原子读写,并使用互斥锁添加保护。

runtime底层的属性getter和setter源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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();
    // retain后引用计数加一
    id value = objc_retain(*slot);
    // 解锁
    slotlock.unlock();
    
    return objc_autoreleaseReturnValue(value);
}
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
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);

    // 判断是需要copy、mutableCopy的结果、还是直接将引用计数加一
    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);
}

为什么不能保证绝对的线程安全

atomic只是对属性的getter/setter方法进行了加锁操作,这种安全仅仅是set/get 的读写安全。这种情况下,单独的读或者写,是一个原子操作。对于单独的原子操作,atomic可以保证其线程安全,但是将两者组合在一起,就不能保证。 这个时候就需要将原子操作的粒度加大,将读和写放在一个事务里进行加锁,这样能避免写入或者读到过期数据。

例子:

比如当线程A setter操作时,这时B线程的setter操作会等待。当A线程的setter结束后,B线程进行setter操作, 然后当A线程需要getter操作时,却有可能获得了在B线程中的值,这就破坏了线程安全

参考文章

iOS Atomic原子操作原理剖析

iOS开发中的11种锁整理

不再安全的 OSSpinLock

Thread safety of weak properties

从源代码理解atomic为什么不是线程安全