浅谈iOS线程原子锁属性atomic

Objective-C中,在声明属性时,通常会不假思索的使用nonatomic来修饰属性,但去面试的时候,总是被问到atomic,使我们不知从何说起,或者说只能简单的回答一句,atomic修饰的属性是「原子性」的,不能同时读写等。本篇文章就来探索一下atomic背后的原理。

1.寻找入口

因为声明属性这个语法是OC特有的,基本能够确定,对属性的操作是在objc源码中实现,所以,为了方便探索,我们直接在objc4-756.2源码中来寻找处理属性的代码。
打开objc-756.2源码,尝试全局搜索setproperty关键字:

oc

跟踪其实现,发现所有为属性赋值的方法最终都会走到reallySetProperty()这个函数:

oc

在工程中新建一个Person类,为了方便研究,Person只有一个name属性,注意它是atomic的:

// Person.h
@interface Person : NSObject

@property (atomic, copy) NSString *name;

@end
复制代码

然后在main()函数中实例化一个Person并为name属性赋值:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        p.name = @"Objc";
    }
    return 0;
}
复制代码

然后在reallySetProperty()这个函数入口打上断点,运行程序,验证一下设置属性时是否会走到这个函数:

可以看到,确实从-[Person setName:]调用到了reallySetProperty()这个函数。

2.源码分析

2.1 赋值

reallySetProperty()的源码:

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);
}
复制代码

参数解析:

  • self:这里就是main()函数中实例化的Person对象p
  • -cmd:这里就是setName:
  • newValue:这就是要赋的新值"Objc"
  • offset:指针偏移量。对象p的指针是它第一个成员isa的地址,而我们的Person只有一个属性,系统会根据这个p的指针 + offset来找到属性对应成员变量_name的内存地址。isa是个指针,所以这里offset = 8
  • atomic:是否是原子性的,此处为YES
  • copy:是否拷贝,此处为YES
  • mutableCopy:是否可变类型拷贝,此处为NO

源码分析:

  • 如果offset = 0,就是修改isa,因此调用object_setClass()
  • 根据偏移量获取属性成员地址slot
  • 根据是否copymutableCopy来拷贝newValue或者objc_retain(newValue)
  • 记录oldValue,在赋值完新值之后释放旧值objc_release(oldValue)
  • 如果是非atomic的,直接对属性赋值
  • 如果是atomic的,先加锁,在赋值,再解锁

其中,加锁使用的spinlock_t上篇文章中提到过,本文不再赘述。
可见,所谓atomic修饰的属性,只是在赋值的时候加了一把锁。

2.2 取值

取值调用的是objc_getProperty()

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);
}
复制代码
  • 如果不是atomic的,直接根据指针和偏移量取到值返回
  • 如果是atomic的,依然是「加锁获取」,并且进行了objc_retain()操作,返回时返回的是自动释放对象objc_autoreleaseReturnValue()

3.注意事项

atomic修饰属性加的锁,仅仅是在settergetter内部加的锁,保证取值/赋值时的线程安全性,但是如果像下面这样使用并不能保证线程安全:

@interface RootViewController ()

@property (atomic) NSInteger number;

@end

@implementation RootViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 线程1
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 1000; i++) {
            self.number = self.number + 1;
            NSLog(@"number: %ld", self.number);
        }
    });
    
    // 线程2
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0; i < 1000; i++) {
            self.number = self.number + 1;
            NSLog(@"number: %ld", self.number);
        }
    });
}

@end
复制代码

属性是atomic修饰的,按理说应该是线程安全的,两个线程各对number做了1000次循环+1,最终numer的值应该是2000,但输出的值却不是期望的2000

// 最后的几行输出
2020-03-07 22:37:21.713683+0800 TestObjC[23813:2171198] number: 1986
2020-03-07 22:37:21.714004+0800 TestObjC[23813:2171198] number: 1987
2020-03-07 22:37:21.714267+0800 TestObjC[23813:2171198] number: 1988
2020-03-07 22:37:21.714541+0800 TestObjC[23813:2171198] number: 1989
2020-03-07 22:37:21.714844+0800 TestObjC[23813:2171198] number: 1990
2020-03-07 22:37:21.715027+0800 TestObjC[23813:2171198] number: 1991
2020-03-07 22:37:21.715442+0800 TestObjC[23813:2171198] number: 1992
复制代码

以上是输出的最后几行,最终的值只加到1992。这是因为两个线程在并发的调用settergetter,在settergetter内部是加了锁,但是在做+1操作的时候并没有加锁,导致在某一时刻,线程一调用了getter取到值,线程2恰好紧跟着调用了getter,取到相同的值,然后两个线程对取到的值分别+1,再分别调用setter,使得两次setter其实赋值了相等的值。

  • 因此使用atomic修饰属性时对属性的操作是否是线程安全的,虽然在内部加了锁,但并不能保证绝对的线程安全。

Guess you like

Origin juejin.im/post/7066778804814872613