zoukankan      html  css  js  c++  java
  • Atomic原子操作原理剖析

    前言

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

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

    入门教程中一般都建议使用非原子操作,因为新手大部分操作都在主线程,用不到线程安全的特性,大量使用还会降低执行效率。

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


    原理

    属性的实现

    首先我们研究一下属性包含的内容。通过查阅源码,其结构如下:

    struct property_t {
        const char *name;       //名字
        const char *attributes; //特性
    };
    

    属性的结构比较简单,包含了固定的名字和元素,可以通过 property_getName 获取属性名,property_getAttributes 获取特性。

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

    那到底是怎么实现原子操作的呢? 通过引入runtime,我们能调试一下调用的函数栈。

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

    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)。

    互斥锁可以解释为线程获取锁,发现锁被占用,就向系统申请锁空闲时唤醒他并立刻休眠。

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

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

    自旋锁的坑

    但是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

  • 相关阅读:
    asp.net 通过js调用webService注意
    身份证号码验证 类
    char值码对应大全
    C# 让textbox 只能输入数字的方法
    table嵌套循环数据
    拆分字符串
    angular的路由配置
    js对象数组(JSON) 根据某个共同字段 分组
    当前时间的后七天
    ajax提交时“加载中”提示的处理方法
  • 原文地址:https://www.cnblogs.com/vanch/p/10192002.html
Copyright © 2011-2022 走看看