TLDR:
函数名 | 函数独自的线程安全性 | 总体运用的线程安全性 | (non)atomic 之间功能距离 |
---|---|---|---|
setProperty | 依据 policy 决议 | 依据标识符决议 | 较大 |
getProperty | 依据 policy 决议 | ||
setAssociateObject | 始终线程安全 | 依据标识符决议 | 较小 |
getAssociateObject | 依据 policy 决议 |
起因
西瓜最近在做事务的 Model 治理,事务的 Model 总会有许多的特点是经过在 Category 中添加 AssocitedObject 来完结的。关于多个复杂场景一起运用的 Model 同步结构,就需求一起对功能与稳定性都有所考量。因而进行一些剖析。
本文的剖析引荐对线程安全只知其名不知其完结的同学观看。网上关于 AssociatedObject 这个老八股也有许多解析,但其实没有与 Property 对应做出比照,也没有对其中到底是如何规划线程安全作出论说,理清思路仍是花了很久,探寻思路供咱们参阅。
Set函数
AssociationsManager 结构
图来源于 冬瓜
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 的没有加锁。那咱们就来造一种黄色会溃散的场景。
这儿咱们规划的场景是多线程对一个特点一起设置。
想象一下多线程对一个特点一起设置的状况,咱们首要在线程A处获取到了执行第一步代码后的oldValue,然后此时线程切换到了B,B也获得了第一步后的 oldValue
,所以此时就有两处持有 oldValue
。然后无论是 线程A 或许 线程B 执行到最终都会执行 objc_release(oldValue)
,对同一块内存区域开释两次,就会产生溃散。
但假如加锁了就没有这个问题了。后边拿到的 oldValue
已经是前面一次 set
设置的目标了。不存在一个目标 release
多次。
也便是说,oldValue
的设置是有必要只要一个线程能一起进入的,即需求经过在锁内确保线程安全。
剖析之后咱们发现,对 oldValue
的 release
并不涉及竞赛,那么依据锁最小准则,为了尽或许得优化功能,咱们就把 oldValue
的开开释在 lock 外。
同理,setAssociateObject
也是相同,而且咱们发现:setAssociateObject
只要对应 setProperty
中 atomic
的分支。那我觉得能够阐明 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
这儿也是一个 setAssociateObject
与 setProperty
在表现上不同。
最终咱们再类比下咱们正常会自己写的线程安全的 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_RETAIN 与 GETTER_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 的场景
可是很明显,咱们写一段代码一起有 Set,有 Get 就会发现状况不对,实践上是会溃散的。因而咱们从定论倒推,来剖析下原因。
更正过错
咱们来简化一下 getAssociateObject 的代码。
atomic 的 的标识符会多走:一次retain,一次autorelease。
这儿会有几个问题
-
拿到 value 是在锁内,那为啥不是线程安全的?一般咱们的 get 函数不都是拿到 value,再return?
这锁莫非不保熟? -
从成果倒推,加了 retain 跟 autorelease 就能确保线程安全,这是为什么?
-
为什么 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
}
咱们由此能够发现:getter 需求在锁内进行提早的 retain,避免 setter 提早对 oldValue 调用 objc_release() 。因而解答了为什么单单加锁,不能确保线程安全。
得到定论
咱们再回到 get_assciate 的办法,不管是不是atomic的,都加锁了。但加锁还不能确保安全。因而单看 get_assciate 办法会有幻觉,感觉是 retain + autorelease 办法确保了线程安全,其实不是的。两个要素加起来才能确保。**
因而这就能够回答上面三个问题了。
-
为什么要在get的时分 retain + autorelease,因为不这样会崩,release或许会走到 retain 前面。
-
那为什么 retain 有必要在锁内,autorelease 能够在锁外?因为 get 拿到value之后,有必要在 set 办法 objc_release 之前调用 retain,否则仍是崩。一起这儿就算是 nonatomic 也需求加锁是为了避免多线程操作写坏 AssociationsHashMap 。
-
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_XXX
,objc_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…