ios 原子属性nonatomic/atomic

原子属性 (Atomic Properties)

你曾经好奇过 Apple 是怎么处理 atomic 的设置/读取属性的么?至今为止,你可能听说过自旋锁 (spinlocks),信号量(semaphores),锁 (locks),@synchronized 等,Apple 用的是什么呢?因为 Objctive-C 的 runtime 是开源的,所以我们可以一探究竟。

在MRC下, 一个非原子的 setter 看起来是这个样子的:

- (void)setUserName:(NSString *)userName {
	if (userName != _userName) {
		[userName retain/copy]; // 根据属性的内存管理语义
		[_userName release];
		_userName = userName;
	}
}

这是一个MRC下的retain/release 的版本,ARC 生成的代码和这个看起来也是类似的。当我们看这段代码时,显而易见要是 setUserName: 被并发调用的话会造成麻烦。我们可能会释放 _userName 两次,这回使内存错误,并且导致难以发现的 bug。

对于任何没有手动实现的属性,编译器都会生成一个 objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 的调用。在我们的例子中,这个调用的参数是这样的:

一共6个参数, 下面有具体实现

objc_setProperty_non_gc(self, _cmd,
(ptrdiff_t)(&_userName) - (ptrdiff_t)(self),
userName, NO, NO);

ptrdiff_t 可能会吓到你,但是实际上这就是一个简单的指针算术,因为其实 Objective-C 的类仅仅只是 C 结构体而已。

objc_setProperty 调用的是如下方法:

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);
}

其实方法实际做的事情非常直接,

  • 非原子性,
    • 用一个临时变量存放旧值,
    • 用新值给这个内存区域赋值
    • 释放旧值
  • 原子性的情况
    • 先用旧值作为key, 从字典中取出一把锁
    • 加锁
    • 用一个临时变量存放旧值,
    • 用新值给这个内存区域赋值
    • 解锁
    • 释放旧值

加锁使用了 PropertyLocks 中的自旋锁spinlock_t中的 1 个来给操作上锁。这是一种务实和快速的方式, set/get本身是一个非常轻量级的操作, 忙等待就行了.

我当时的runtime版本是objc4-750, 看到这个自旋锁之后还在想这个不是已经不推荐了吗, 怎么苹果自己还在用, 又跑到官网上看了一个最新的objc4-818.2确认了一下, 最新的818.2也是使用的自旋锁, 看来苹果对这个atomic不怎么上心了.  Runtime官方开源地址

PropertyLocks是一个hashMap,类似于字典,内部使用一个固定长度的数组存放锁,runtime初始化的时候就会创建完成锁并放入到数组中, 在iOS真机上数组数量固定为8,其他设备为64, 也就是说, iOS的PropertyLocks最多提供8个自旋锁给属性的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);
}

get方法也很简单,通过偏移量取出对应地址的值,

  • 如果是nonatomic的话, 直接返回, 结束
  • 如果是atomic的话,
    • 会先从一个字典中取锁, 以返回值作为key取出对应的锁,
    • 用取出的锁进行加锁,
    • 对返回值进行一次retain
    • 解锁
    • 把返回值加入自动释放池, 返回, 

通过set/get源码的阅读, 我们也可以理解为什么苹果不推荐使用atomic了.

  1. PropertyLocks是一个全局生效的字典, 最多提供8个自旋锁给atomic加锁解锁, 当一个项目中有几万个属性都是原子性的时候, 很多属性都会对应到同一把锁上, 那么这个属性就得等其他毫不相关的属性完成读写, 自己才能进行操作, 而且set/get是一个特别高频的操作.
  2. PropertyLocks使用的是自旋锁, 自旋锁的特点是忙等待, 当有几万的属性对应到8把锁上, 忙等待就是一个非常常见的操作, 忙等待对cpu的消耗很大, 手机发烫, 效率变低, 而且这样的忙等待是无意义的.
  3. atomic只能针对特定场景保证线程安全, 存在局限性. 只能保证set/get的线程安全, 对于更大范围的线程安全是无法保证的. 
            举一个很简单的例子,​​​假设定义属性 NSInteger i 是原子的,对i进行 i = i + 1;
    这个操作就是不安全的。因为原子性只能保证读写安全,而该表达式需要三步操作:
    1.先进行get, 读取i的值存入寄存器;
    2.将寄存器的值加1;
    3.使用寄存器修改后的值给i赋值;
    atomic只能保证1和3是线程安全的, 如果在第1步完成的时候,i被其他线程修改了,那么表达式执行的结果就会与预期的不一样,也就是不安全的。所以要解决这样的线程安全问题, 只能对 整个表达式进行加锁, 单纯对i 设置atomic达不到预期的.

综上3点, atomic在大范围使用时效率低下, 而且效果不太好, 存在局限性, 这可能就是苹果不推荐atomic的原因了.

虽然这些方法没有定义在任何公开的头文件中,但我们还是可用手动调用他们。我不是说这是一个好的做法,但是知道这个还是蛮有趣的,而且如果你想要同时实现原子属性和自定义的 setter 的话,这个技巧就非常有用了。

// 手动声明运行时的方法
extern void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset,
id newValue, BOOL atomic, BOOL shouldCopy);

extern id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic);


#define PSTAtomicRetainedSet(dest, src) objc_setProperty(self, _cmd, (ptrdiff_t)(&dest) - (ptrdiff_t)(self), src, YES, NO)

#define PSTAtomicAutoreleasedGet(src) objc_getProperty(self, _cmd, (ptrdiff_t)(&src) - (ptrdiff_t)(self), YES)

为何不用 @synchronized ?

你也许会想问为什么苹果不用 @synchronized(self) 这样一个已经存在的运行时特性来锁定属性?? 主要原因还是效率问题.

你可以看看苹果的源代码,就会发现其实发生了很多的事情。Apple 使用了最多三个加/解锁序列,还有一部分原因是他们也添加了异常开解(exception unwinding)机制。相比于更快的自旋锁方式,@synchronized实现要慢得多。由于设置某个属性一般来说会相当快,因此自旋锁更适合用来完成这项工作。@synchonized(self) 更适合使用在你需要确保在发生错误时代码不会产生死锁,而是抛出异常的时候。

多线程下出错案例分析

if (self.contents) {
    CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL, 
      (__bridge CFStringRef)self.contents, NULL);
    // 渲染字符串
}

多线程下存在contents属性在通过检查之后却又被设成了nil而导致EXC_BAD_ACCESS崩溃。捕获这个变量就可以简单修复这个问题。

NSString *contents = self.contents;
if (contents) {
    CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL, 
      (__bridge CFStringRef)contents, NULL);
    // 渲染字符串
}

类似的问题也可能出现在block中, 但是这时候使用局部变量接收不能解决问题, 还是需要通过传统的加锁/解锁来处理, 明明判断block有值才执行,为什么还是crash


数组,字典尽量用不可变的版本,没有多线程并发的问题,
如果需要添加/删除操作,可以使用局部变量作为可变版本,可变版本的修改完成后进行copy.
注意:对不可变的属性进行赋值的操作也要保证线程安全

// 方案1:使用atomic,下面的方法就没有必要加锁了,
// 方案2:此处用nonatomic,在set/get的地方加锁,
// 一开始尝试了只在set加锁,get不加锁,发现不可以,必须set/get都加锁才能线程安全
@property (nonatomic, strong) NSArray *dataArray;

- (void)addDelegate:(id<NSObject>)delegate {
    @synchronized(self) {
        NSMutableArray *tempArray = [NSMutableArray arrayWithArray:self.dataArray];
        [tempArray addObject:delegate];
        self.dataArray = [tempArray copy];
    }
}

- (void)removeDelegate:(id<NSObject>)delegate {
    @synchronized(self) {
        NSMutableArray *tempArray = [NSMutableArray arrayWithArray:self.dataArray];
        [tempArray removeObject:delegate];
        self.dataArray = [tempArray copy];
    }
}

- (void)removeAllDelegates {
    @synchronized(self) {
        self.dataArray = nil;
    }
}

- (void)callDelegate {
    NSArray *array = nil;
    @synchronized(self) {
        array= [NSArray arrayWithArray:self.dataArray];
    }
    [array enumerateObjectsUsingBlock:^(id<NSObject> delegate, NSUInteger idx, BOOL *stop) {
        // 调用delegate
    }];
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值