AssociatedObject 源码分析:如何实现线程安全?

AssociatedObject 源码分析:如何实现线程安全?

TLDR:

函数名 函数独自的线程安全性 总体运用的线程安全性 (non)atomic 之间功能距离
setProperty 依据 policy 决议 依据标识符决议 较大
getProperty 依据 policy 决议
setAssociateObject 始终线程安全 依据标识符决议 较小
getAssociateObject 依据 policy 决议

起因

西瓜最近在做事务的 Model 治理,事务的 Model 总会有许多的特点是经过在 Category 中添加 AssocitedObject 来完结的。关于多个复杂场景一起运用的 Model 同步结构,就需求一起对功能与稳定性都有所考量。因而进行一些剖析。

本文的剖析引荐对线程安全只知其名不知其完结的同学观看。网上关于 AssociatedObject 这个老八股也有许多解析,但其实没有与 Property 对应做出比照,也没有对其中到底是如何规划线程安全作出论说,理清思路仍是花了很久,探寻思路供咱们参阅。

Set函数

AssociationsManager 结构

AssociatedObject 源码分析:如何实现线程安全?

图来源于 冬瓜

unordered_map 是STL 函数,不自带线程安全,因而 _map 也并不自带线程安全。

AssociationsManager 是靠 spinlock 完结的。也便是说 AssociationsManager 经过一个自旋锁 spinlock_t 确保对 AssociationsHashMap 的操作是线程安全的,即每次只会有一个线程对 AssociationsHashMap 进行操作。这儿注意,说的是 AssociationsManager 是线程安全的,不是说 AssociateObject 是线程安全的。

class AssociationsManager {
    static spinlock_t _lock; // static,大局共用一个
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { _lock.lock(); } // 初始化函数,创立目标时调用
    ~AssociationsManager()  { _lock.unlock(); } // 析构函数,开释目标时调用
    AssociationsHashMap &associations() { // 有必要经过实例办法去获取到 _map
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

咱们能够看到在创立 AssociationsManager 的时分就会持有锁,开释 AssociationsManager 的时分就会开释锁。而假如你要拿到 AssociationsHashMap 就有必要要创立 AssociationsManager 目标。

这儿给不了解 C++ 的同学解说下,C++ 能够在 Stack 上创立目标,而不用经过 new/malloc 的方法分配到 Heap 上。

RAII && 锁的运用

光这么描述或许还有些苍茫,咱们接下来看 runtime 是怎么运用这把锁的。

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    {
        // 创立AssociationsManager目标,隐含lock(),并非new的方法创立
        // 在stack上
        AssociationsManager manager;
        // 在manager取_map成员,其实是一个map类型的映射
        AssociationsHashMap &associations(manager.associations());
        // 实践set,先省掉,后边会详细介绍
    }// C++,脱离Scope就开释AssociationsManager,隐含unlock()
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

这个规划思路很有意思,是一种巧妙确保线程安全的办法,每个线程都要经过AssociationsManager 目标 去拿到实践是 static 的东西,经过 Scope 对AssociationsManager 目标 的开释来控制。这样就能够避免咱们忘记去 unlock(),或许提早 return 的时分忘记 unlock() 的工作产生。

这儿还有一个小细节是:并非一切操作都是在锁里完结的,能够看到对 old_association 的开释是在锁外的。原因咱们后边也会介绍到,现在只需求有个印象就行了。

这种类似的规划其实有特定的名字:RAII,全称资源获取即初始化(英语:Resource Acquisition Is Initialization)。RAII要求,资源的有效期与持有资源的目标的生命期(英语:Object lifetime)严格绑定,即由目标的结构函数完结资源的分配(英语:Resource allocation (computer))(获取),一起由析构函数完结资源的开释。在这种要求下,只要目标能正确地析构,就不会出现资源走漏(英语:Resource leak)问题。

那么既然这个这么好,咱们 iOS 开发能不能也整一个呢?答案是不能直接用,但能够有代替计划。

OC 是因为目标或许被参加 autoreleasepool 中,一旦被参加 pool 了,目标的开释不跟 scope 挂钩,是跟 pool 什么时分被 pop 挂钩;OC 也没有在 stack 上创立 目标的能力;OC(Swift也是) 目前运用较多的 ARC 是 依据运用状况的(use-based),也有实质的不同。

Swift 变量也不是脱离 scope 才开释的,是最终一次运用之后就不再确保存在的,尽管实践中或许会拖延开释的事件,但依赖现在的不确定拖延时机去写代码是不安全的,容易跟着后续的编译器优化而产生极难排查的问题。详细能够看:【老司机精选】Swift 中的 ARC 机制: 从根底到进阶。

当然 OC 中也有 @onExit{} , Swift 中也有 defer 能够达到在退出 Scope 时进行一些资源开释。这个就不在这儿论说了,咱们很或许都运用过。

剖析 setAssociateObject 的主途径

详细代码有删减,咱们主要剖析有 old_association ,并替换新的 new_value 的这条链路。

删减之后如下。一些边缘Case就隐藏了,有爱好的同学能够自己去研讨关于 DISGUISE 、替换/创立 AssociateObject 相关逻辑。

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // 遍历链表去拿到j,j 便是一个遍历器,j->second 便是实践的目标
            ObjectAssociationMap::iterator j = findJ();
            // 重要逻辑
            old_association = j->second;
            j->second = ObjcAssociation(policy, new_value);
        }
        // other code
        // 包含 新建节点、删除节点等
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

最重要的便是在锁内,有一个把 old_associate 取出,把新的 ObjcAssociation 赋值的过程。这个过程在锁内,所以确保了线程安全。

最终脱离锁的区域,并开释 old_associate

那这个没有比照,咱们尽管有感觉 setAssociateObject 中锁内的操作跟是否 NONATOMIC 的 policy 没有关系,但这个还不是实锤。那么咱们找一个别的参阅。

参阅 setProperty

类比下 reallySetProperty 函数,发现 reallySetProperty 仍是区分 atomic 的操作的。但原子性的操作都是共同的,甚至包含在锁外开释 old 目标

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();
    }
    // release 目标也在锁外
    objc_release(oldValue);
}

咱们来看下 nonatomic 跟 atomic 的代码,其实都是共同的,差异仅仅 atomic 的部分在锁内,nonatomic 的没有加锁。那咱们就来造一种黄色会溃散的场景。

这儿咱们规划的场景是多线程对一个特点一起设置。

AssociatedObject 源码分析:如何实现线程安全?

想象一下多线程对一个特点一起设置的状况,咱们首要在线程A处获取到了执行第一步代码后的oldValue,然后此时线程切换到了B,B也获得了第一步后的 oldValue,所以此时就有两处持有 oldValue 。然后无论是 线程A 或许 线程B 执行到最终都会执行 objc_release(oldValue),对同一块内存区域开释两次,就会产生溃散。

但假如加锁了就没有这个问题了。后边拿到的 oldValue 已经是前面一次 set 设置的目标了。不存在一个目标 release 多次。

也便是说,oldValue 的设置是有必要只要一个线程能一起进入的,即需求经过在锁内确保线程安全。

剖析之后咱们发现,对 oldValuerelease 并不涉及竞赛,那么依据锁最小准则,为了尽或许得优化功能,咱们就把 oldValue 的开开释在 lock 外。

同理,setAssociateObject 也是相同,而且咱们发现:setAssociateObject 只要对应 setPropertyatomic 的分支。那我觉得能够阐明 setAssociateObject 办法必定是线程安全的了,咱们用 demo 实践验证一下。

验证

这儿能够做一个比照,假如你是多线程 setProperty ,是会崩的。一起溃散的原因也与咱们的剖析共同,是某个目标被过度开释。

@property(nonatomic, strong) NSMutableData *data;
for (int i = 0; i < 100000; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        self.data = [[NSMutableData alloc] init];
    });
}
// error for object: pointer being freed was not allocated。

但假如你是多线程 setAssocited,是不会崩的。也就验证了源码剖析。

for (NSInteger i = 0; i < 1000000; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [self setAssocitedObject:[NSObject new]];
    });
}
- (void)setAssocitedObject:(NSObject *)object {
  objc_setAssociatedObject(self, @selector(associtedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// fine, no crash

这儿也是一个 setAssociateObjectsetProperty 在表现上不同。

最终咱们再类比下咱们正常会自己写的线程安全的 set 办法,objc_release 办法是ARC帮咱们主动加上的,但也是在 lock 外面的。所以咱们正常的写法也是暗含这个优化逻辑在里面的。

- (void)setSomeString:(NSString *)aString
{
    NSString *old = nil
    @synchronized(self)
    {
        if (someString != aString)
        {
            old = someString;
            someString = [aString copy];
        }
    }
    [old release]; // 编译器会帮咱们主动加上的
}

当然 @synchronized 这把锁不建议咱们运用,会有许多问题,能够用别的锁。这儿仅仅偷懒了举个例子。

Get函数

经过上面的剖析,我觉得我又行了,那剖析一半我觉得是不可的,肯定也得把 get 的部分给剖析了。

首要这儿咱们需求看看咱们了解的 5 个枚举在 runtime 内部是如何运用的。咱们能够看到实践上会拆成 SETTTER 跟 GETTER 的结合。

// runtime.h
enum { 
    OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */ 
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. * The association is not made atomically. */ 
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied. * The association is not made atomically. */ 
    OBJC_ASSOCIATION_RETAIN = 01401 = 0x0301, /**< Specifies a strong reference to the associated object. * The association is made atomically. */ 
    OBJC_ASSOCIATION_COPY = 01403 = 0x0303 /**< Specifies that the associated object is copied. * The association is made atomically. */ 
};
// objc-references.mm
enum { 
    OBJC_ASSOCIATION_SETTER_ASSIGN = 0, 
    OBJC_ASSOCIATION_SETTER_RETAIN = 1, 
    OBJC_ASSOCIATION_SETTER_COPY = 3, // NOTE: both bits are set, so we can simply test 1 bit in releaseValue below. 
    OBJC_ASSOCIATION_GETTER_READ = (0 << 8), 
    OBJC_ASSOCIATION_GETTER_RETAIN = (1 << 8), 
    OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8) 
}; 

因而咱们能够从枚举处得到:

OBJC_ASSOCIATION_RETAIN_NONATOMIC == OBJC_ASSOCIATION_SETTER_RETAIN

OBJC_ASSOCIATION_RETAIN == OBJC_ASSOCIATION_SETTER_RETAIN | OBJC_ASSOCIATION_GETTER_RETAIN | OBJC_ASSOCIATION_GETTER_AUTORELEASE

也便是说 OBJC_ASSOCIATION_RETAIN 比较 OBJC_ASSOCIATION_RETAIN_NONATOMIC 多了 GETTER_RETAINGETTER_AUTORELEASE

这儿能够发现在枚举的语义中。是否 NONATOMIC,在 SETTER 没有任何不同,仅仅在 GETTER 中有所不同。这个也契合咱们前面的剖析。

然后咱们再看get的代码:

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        // lock()
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain); // retain 一次,锁内
            }
        }
        // unlock()
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease); // autorelease 一次,锁外
    }
    return value;
}

过错定论的得出

ATOMATIC 会额外运转的两行代码,一次锁内的 retain,一次锁外的 autorelease

全体从 AssociationsManager 中获取详细值的操作也都在锁内,感觉没啥不安全的。

一起因为剖析 Set 函数的时分,咱们考虑了 多个线程一起 Set 的状况,这儿我也想象了 多个线程一起 Get 的状况,剖析后觉得没什么 Bad Case。不要笑,其时我的确绕进去了,这儿实践应该考虑的是 多个线程一起Set Get 的场景

AssociatedObject 源码分析:如何实现线程安全?

可是很明显,咱们写一段代码一起有 Set,有 Get 就会发现状况不对,实践上是会溃散的。因而咱们从定论倒推,来剖析下原因。

更正过错

咱们来简化一下 getAssociateObject 的代码。

AssociatedObject 源码分析:如何实现线程安全?

atomic 的 的标识符会多走:一次retain,一次autorelease。

这儿会有几个问题

  1. 拿到 value 是在锁内,那为啥不是线程安全的?一般咱们的 get 函数不都是拿到 value,再return?这锁莫非不保熟?

  2. 从成果倒推,加了 retain 跟 autorelease 就能确保线程安全,这是为什么?

  3. 为什么 autorelease 能够放在锁外不用放在锁内?

参阅 getProperty

要了解这个问题,咱们需求先回到正常get特点的时分,看看getProperty的完结

id objc_getProperty_non_gc(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);
    // nonatomic,直接return
    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 的时分,先加锁,再 retain ,解锁,再 autorelease 。是不是跟 _object_get_associative_reference 有点像啊?

set 的时分,咱们剖析 bad case 的时分是用 一起有 多个 set 的,但剖析 get 的时分,不能剖析有多个 get ,因为没有修改,本来就没问题。所以咱们应该是考虑 set 跟 get 混用的状况。

因而咱们先剖析 get_property 的时分,为什么要 retain (锁内) + autorelease (锁外)。假如我仅仅加锁,不retain + autorelease 会产生什么?

// wrong 完结
id objc_getProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks[slot];
    slotlock.lock();
    id value = objc_retain(*slot); // delete this line
    id value = *slot; // instead
    slotlock.unlock();
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value); // delete this line
    return value; // instead
}

AssociatedObject 源码分析:如何实现线程安全?

咱们由此能够发现:getter 需求在锁内进行提早的 retain,避免 setter 提早对 oldValue 调用 objc_release() 。因而解答了为什么单单加锁,不能确保线程安全

得到定论

咱们再回到 get_assciate 的办法,不管是不是atomic的,都加锁了。但加锁还不能确保安全。因而单看 get_assciate 办法会有幻觉,感觉是 retain + autorelease 办法确保了线程安全,其实不是的。两个要素加起来才能确保。**

因而这就能够回答上面三个问题了。

  1. 为什么要在get的时分 retain + autorelease,因为不这样会崩,release或许会走到 retain 前面。

  2. 那为什么 retain 有必要在锁内,autorelease 能够在锁外?因为 get 拿到value之后,有必要在 set 办法 objc_release 之前调用 retain,否则仍是崩。一起这儿就算是 nonatomic 也需求加锁是为了避免多线程操作写坏 AssociationsHashMap 。

  3. for performance,锁最小准则。

所以 property 的 get 更暴力一些,演都不演了直接 return了。正因如此,property的 nonatomic/atomic 之间功能距离较大,而 AssociateObject 的就距离不这么大,正常运用也不会有太多功能问题。

剖析完毕。因而一开始我就在考虑为什么 retain + autorelease 办法能确保线程安全属实是把我自己绕进去了。经过剖析 property 的 get/set 有助于咱们更好的了解 AssociateObject 的 get/set。

研讨线程安全的通用办法

研讨 set 办法时,需求考虑一起多个线程 set,也能够 set 的一起 有 get。

研讨 get 办法时,则必定不能考虑多个线程一起 get,因为这个是没有实践操作含义的,应该考虑 get 的一起也有 set 操作。

定论

只set,不get,是正常的,不会崩。但这个不契合实践, 不get你存他干什么。

不set,疯狂get,是正常的。但这个其实也是不契合实践, 不加锁也是能确保的。

又set,又get,因为get不是线程安全的(假如你的标识符号是 NONATOMIC ),会崩;又set,又get,get是线程安全的(假如你的标识符号是 ATOMIC ),不会崩。

不管你的标识符是 OBJC_ASSOCIATION_XXX_NONATOMIC 仍是 OBJC_ASSOCIATION_XXXobjc_setAssociatedObject 函数都是 线程安全 的;可是 objc_getAssociatedObject 函数则是依据你的标识符来决议是否是 线程安全 的。

运用 AssocitedObject 时依据事务场景决议是否运用对应标识符,毕竟运用时又有 set 又有 get 才是常态,独自看 set 或许 get 不具有实践含义。

Property的 (non)atomic 之间功能距离较大,而 AssociateObject 的 (non)atomic 之间就距离不大,正常运用也不会有太多功能问题。

最终关于 AssociatedObject/Property 相关的源码其实还有许多常识,例如 StripeMap 等等,其实都很有意思,搞清楚这些更多是带来自己探索问题的思路,一个个相对独立的常识点的学习其实仅仅常识的堆集,仍是要把握自己学习探索的方法,假如单纯看一个函数想不通原因,能够寻找别的类似机制的函数做参阅,往往能够获得突破。

参阅资料

浅谈Associated Objects

相关目标 AssociatedObject 彻底解析

opensource.apple.com/source/objc…

stackoverflow.com/questions/2…

【老司机精选】Swift 中的 ARC 机制: 从根底到进阶

en.wikipedia.org/wiki/Spinlo…