iOS知识小集

atomic的实现机制

2018-11-02  本文已影响0人  CharmecarWang

atomic作用:多线程下将属性设置为atomic可以保证读取数据的一致性。因为他将保证数据只能被一个线程占用,也就是说一个线程对属性进行写操作时,会使用自旋锁锁住该属性。不允许其他的线程对其进行读取操作了。
但是它有一个很大的缺点:因为它要使用自旋锁锁住该属性,因此它会消耗更多的资源,性能会很低。要比nonatomic慢20倍。

内部实现:property 的 atomic 是采用 spinlock_t (自旋锁)实现的。
// getter方法

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) 
{
    // ...
    if (!atomic) return *slot;

    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    // ...
}

//setter方法

// setter
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    // ...
    if (!atomic) 
{
        oldValue = *slot;
        *slot = newValue;
    } 
else 
{
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    // ...
}

小结

简而言之,atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。

指针Property指向的内存区域

property可以分为值类型和引用类型。值类型就是一些基本的数据类型。而引用类型就是指的各种对象。声明为指针,指向这个属性所在的内存区域。
因此当我们访问一个属性的时候,有可能访问的是指针本身,还有可能访问的是这个指针指向的内存区域。
如:

self.userName = @"nick";

这里访问的就是指针本身,对指针进行赋值。

[self.userName rangeOfString:@"peak"];

是在访问指针指向的字符串所在的内存区域。
指针本身也是占用内存的,是固定的8个字节。
而指针指向的内存区域占用的内存就无法固定了,有可能很大,也有可能很小。我们暂且定为n。

下面举例说明一下:

@property (atomic) NSArray *array;

atomic修饰的实际上是这个指针,也就是占8个字节内存的指针,因此就不可能随意使用多线程来操作这块内存的。因为这块内存是原子性的。是线程安全的。
真正不安全的是指针指向的那块内存区域,他是非原子性的,当多个线程去操作这块内存的时候,就会出现不安全的情况。
下面我们再看两个例子:

@property (atomic, strong) NSString*                 stringA;

//thread A
for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}

//thread B
for (int i = 0; i < 100000; i ++) {
    if (self.stringA.length >= 10) {
        NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
    }
    NSLog(@"Thread B: %@\n", self.stringA);
}

因为atomic修饰的是指针区域,而不是内存区域,但是我们通过self.stringA.lengthsubstringWithRange访问的是property的内存区域,在前一刻读length的时候self.stringA = @"a very long string";,下一刻取substring的时候线程A已经将self.stringA = @"string";,立即出现out of bounds的Exception,crash。主要原因就是self.stringA.length >= 10NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];代码之间内存区域被其他线程修改了。

同样的场景还存在对集合类操作的时候,比如:

@property (atomic, strong) NSArray*                 arr;

//thread A
for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
        self.arr = @[@"1", @"2", @"3"];
    }
    else {
        self.arr = @[@"1"];
    }
    NSLog(@"Thread A: %@\n", self.arr);
}

//thread B
for (int i = 0; i < 100000; i ++) {
    if (self.arr.count >= 2) {
        NSString* str = [self.arr objectAtIndex:1];
    }
    NSLog(@"Thread B: %@\n", self.arr);
}

同理,即使我们在访问objectAtIndex之前做了count的判断,线程B依旧很容易crash,原因也是由于前后两行代码之间arr所指向的内存区域被其他线程修改了。

所以真正要操心的是这一类内存区域的访问,即使用atomatic声明属性也没用。

如何做到多线程安全?(加锁)

为了避免这种crash可以通过一下代码:

//thread A
[_lock lock];
for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
        self.stringA = @"a very long string";
    }
    else {
        self.stringA = @"string";
    }
    NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];

//thread B
[_lock lock];
if (self.stringA.length >= 10) {
    NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
[_lock unlock];

//thread A
[_lock lock];
for (int i = 0; i < 100000; i ++) {
    if (i % 2 == 0) {
        self.arr = @[@"1", @"2", @"3"];
    }
    else {
        self.arr = @[@"1"];
    }
    NSLog(@"Thread A: %@\n", self.arr);
}
[_lock unlock];
    
//thread B
[_lock lock];
if (self.arr.count >= 2) {
    NSString* str = [self.arr objectAtIndex:1];
}
[_lock unlock];

参考链接1
参考链接2

上一篇下一篇

猜你喜欢

热点阅读