iOS多线程安全(一)

多线程访问同一个对象,经常会出现意料之外的结果。

这里就从atomic与nonatomic讲起。

atomic

atomic能从一定程度上保证线程安全,但是大部分的情况下并没不能完全保证线程安全。

首先我们看看如果将一个属性设置为atomic的时候,编译器帮我们做了什么?

  • 生成原子操作的getter和setter方法

当线程A执行setter方法时,线程B如果需要执行getter方法必须等线程ASetter方法结束后才能执行。

atomic内部实现:

// 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();
    }
    // ...
}

// 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();
    // ...
}

对于没法在CPU一次读写操作中完成的setter和getter方法,就需要考虑其在多线程中的安全性,否则可能导致crash。
系统是通过自旋锁的方式来保证CPU多次读写的执行顺序。

比如32位的系统中:
一个Bool值占1字节,可以在一次读写操作中完成赋值或取值,因此是线程安全的。
一个指针占4字节,也可以在一次读写操作中完成赋值或取值,因此是线程安全的。
一个double占8字节,则需要两次读写操作才能完成赋值或取值,因此就会存在一个写操作(需两次写操作才能完成),之后就是读操作的可能,导致异常值的现象。

  • 设置Memory Barrier

对于Objective C的实现来说,几乎所有的加锁操作最后都会设置memory barrier,atomic本质上是对getter,setter加了锁,所以也会设置memory barrier。

memory barrier能够保证内存操作的顺序,按照我们代码的书写顺序来。听起来有点不可思议,事实是编译器会对我们的代码做优化,在它认为合理的场景改变我们代码最终翻译成的机器指令顺序。也就是说如下代码:

self.intA = 0;  //line 1
self.intB = 1;  //line 2

编译器可能在一些场景下先执行line2,再执行line1,因为它认为A和B之间并不存在依赖关系,虽然在代码执行的时候,在另一个线程intA和intB存在某种依赖,必须要求line1先于line2执行。

如果设置property为atomic,也就是设置了memory barrier之后,就能够保证line1的执行一定是先于line2的,当然这种场景非常罕见,一则是出现变量跨线程访问依赖,二是遇上编译器的优化,两个条件缺一不可。这种极端的场景下,atomic确实可以让我们的代码更加多线程安全一点,但我写iOS代码至今,还未遇到过这种场景,较大的可能性是编译器已经足够聪明,在我们需要的地方设置memory barrier了。

nonatomic

大部分情况下,我们会选择nonatomic作为属性的attribute,因为它能有效避免线程锁带来的性能消耗,而大部分的多线程安全问题也需要我们自己来完成线程安全。

参考链接:
http://mrpeak.cn/blog/ios-thread-safety/

你可能感兴趣的:(iOS多线程安全(一))