Atomic原子操做原理剖析

前言

绝大部分 Objective-C 程序员使用属性时,都不太关注一个特殊的修饰前缀,通常都无脑的使用其非默认缺省的状态,他就是 atomicios

@interface PropertyClass

@property (atomic, strong)    NSObject *atomicObj;  //缺省也是atomic
@property (nonatomic, strong) NSObject *nonatomicObj;

@end
复制代码

入门教程中通常都建议使用非原子操做,由于新手大部分操做都在主线程,用不到线程安全的特性,大量使用还会下降执行效率。git

那他到底怎么实现线程安全的呢?使用了哪一种技术呢?程序员


原理

属性的实现

首先咱们研究一下属性包含的内容。经过查阅源码,其结构以下:github

struct property_t {
    const char *name;       //名字
    const char *attributes; //特性
};
复制代码

属性的结构比较简单,包含了固定的名字和元素,能够经过 property_getName 获取属性名,property_getAttributes 获取特性。算法

上例中 atomicObj 的特性为 T@"NSObject",&,V_atomicObj,其中 V 表明了 strongatomic 特性缺省没有显示,若是是 nonatomic 则显示 N安全

那究竟是怎么实现原子操做的呢? 经过引入runtime,咱们能调试一下调用的函数栈。markdown

能够看到在编译时就把属性特性考虑进去了,Setter 方法直接调用了 objc_setPropertyatomic 版本。这里不用 runtime 去动态分析特性,应该是对执行性能的考虑。async

static inline void reallySetProperty(id self, SEL _cmd, 
    id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
    //偏移为0说明改的是isa
    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);
}

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    // 取isa
    if (offset == 0) {
        return object_getClass(self);
    }

    // 非原子操做直接返回
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // 原子操做自旋锁
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // 出于性能考虑,在锁以外autorelease
    return objc_autoreleaseReturnValue(value);
}
复制代码

什么是自旋锁呢?

锁用于解决线程争夺资源的问题,通常分为两种,自旋锁(spin)和互斥锁(mutex)。函数

互斥锁能够解释为线程获取锁,发现锁被占用,就向系统申请锁空闲时唤醒他并马上休眠。oop

自旋锁比较简单,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。

原子操做的颗粒度最小,只限于读写,对于性能的要求很高,若是使用了互斥锁势必在切换线程上耗费大量资源。相比之下,因为读写操做耗时比较小,可以在一个时间片内完成,自旋更适合这个场景。

自旋锁的坑

可是iOS 10以后,苹果由于一个巨大的缺陷弃用了 OSSpinLock 改成新的 os_unfair_lock

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

描述引用自 ibireme 大神的文章。

个人理解是,当低优先级线程获取了锁,高优先级线程访问时陷入忙等状态,因为是循环调用,因此占用了系统调度资源,致使低优先级线程迟迟不能处理资源并释放锁,致使陷入死锁。

那为何原子操做用的仍是 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,实际测试二者的效率差很少。


问答

atomic的实现机制

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

为何不能保证绝对的线程安全?

单独的原子操做绝对是线程安全的,可是组合一块儿的操做就不能保证。

- (void)competition {
    self.intSource = 0;

    dispatch_async(queue1, ^{
      for (int i = 0; i < 10000; i++) {
          self.intSource = self.intSource + 1;
      }
    });

    dispatch_async(queue2, ^{
      for (int i = 0; i < 10000; i++) {
          self.intSource = self.intSource + 1;
      }
    });
}
复制代码

最终获得的结果确定小于20000。当获取值的时候都是原子线程安全操做,好比两个线程依序获取了当前值 0,因而分别增量后变为了 1,因此两个队列依序写入值都是 1,因此不是线程安全的。

解决的办法应该是增长颗粒度,将读写两个操做合并为一个原子操做,从而解决写入过时数据的问题。

os_unfair_lock_t unfairLock;
- (void)competition {
    self.intSource = 0;

    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    dispatch_async(queue1, ^{
      for (int i = 0; i < 10000; i++) {
          os_unfair_lock_lock(unfairLock);
          self.intSource = self.intSource + 1;
          os_unfair_lock_unlock(unfairLock);
      }
    });

    dispatch_async(queue2, ^{
      for (int i = 0; i < 10000; i++) {
          os_unfair_lock_lock(unfairLock);
          self.intSource = self.intSource + 1;
          os_unfair_lock_unlock(unfairLock);
      }
    });
}
复制代码

总结

经过学习属性的原子性,对系统中锁的理解又加深,包括自旋锁,互斥锁,读写锁等。

原本都觉得实现是自旋锁了,还好留了个心眼多看了一层才发现最终实现仍是互斥锁。这件事也给我一个小教训,查阅源码仍是要刨根问底,只浮于表面的话,可能得不到想要的真相。

引用

能够编译的runtime库

再也不安全的 OSSpinLock