持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
今天我们继续探索@synchronized原理和block的类型及用法。
一. @synchronized原理
1、@synchronized
在main函数中添加:
@autoreleasepool {
NSObject *obj = [NSObject alloc];
@synchronized (obj) {
}
}
复制代码
然后clang main.m文件生成main.cpp.找到main函数: objc底层最终执行的是
objc_sync_enter(_sync_obj); objc_sync_exit(sync_exit);
两个函数。 我们进入objc源码看看:
int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
ASSERT(data);
data->mutex.lock();
} else {
// @synchronized(nil) does nothing
if (DebugNilSync) {
_objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
}
objc_sync_nil();
}
return result;
}
int objc_sync_exit(id obj)
{
int result = OBJC_SYNC_SUCCESS;
if (obj) {
SyncData* data = id2data(obj, RELEASE);
if (!data) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
} else {
bool okay = data->mutex.tryUnlock();
if (!okay) {
result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
}
}
} else {
// @synchronized(nil) does nothing
}
return result;
}
复制代码
我们看到了都创建了SyncData
的结构体。
data->mutex.lock();
递归加锁。threadCount 记录有几个线程使用到了
@synchronized的递归锁。 我们进入id2data
看看源码:
static SyncData* id2data(id object, enum usage why)
{
spinlock_t *lockp = &LOCK_FOR_OBJ(object);//锁
SyncData **listp = &LIST_FOR_OBJ(object);//二级指针,头节点地址
SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
// Check per-thread single-entry fast cache for matching object
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
fastCacheOccupied = YES;
//..........................第一段省略..............................//
}
#endif
// TLS 存储只属于当前线程的数据
// Check per-thread cache of already-owned locks for matching object
//检查每个线程的 cache 去匹配一个对象objcet
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
//..........................第二段省略..............................//
}
\
// Thread cache didn't find anything.
// Walk in-use list looking for matching object
// Spinlock prevents multiple threads from creating multiple
// locks for the same new object.
// We could keep the nodes in some hash table if we find that there are
// more than 20 or so distinct locks active, but we don't do that now.
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
//..........................第三段省略..............................//
}
// Allocate a new SyncData and add to list.
// XXX allocating memory with a global lock held is bad practice,
// might be worth releasing the lock, allocating, and searching again.
// But since we never free these guys we won't be stuck in allocation very often.
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
result->object = (objc_object *)object;
result->threadCount = 1;
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
result->nextData = *listp;
*listp = result;
done:
lockp->unlock();
if (result) {
// Only new ACQUIRE should get here.
// All RELEASE and CHECK and recursive ACQUIRE are
// handled by the per-thread caches above.
if (why == RELEASE) {
// Probably some thread is incorrectly exiting
// while the object is held by another thread.
return nil;
}
//..........................第四段省略..............................//
}
return result;
}
复制代码
&LOCK_FOR_OBJ 和 &LIST_FOR_OBJ
的对应实现。
所以:@synchronized真机上的性能高于模拟器上的性能。
StripedMap 是缓存带spinlock锁能力的类或者结构体
StripedMap
StripedMap 的主要作⽤是⽤来缓存 带spinlock锁能⼒的类或者结构体 。 它的⼯作原理是什么呢?⽐如说这个SyncList,我们想⼀下,如果说系统在全局只初始化⼀张 SyncList⽤来管理所有对象的加锁和解锁操作,其实也是可以。只是,效率会很慢,因为每个对象 在操作这个表的时候,都需要等待其他对象操作完解锁之后才能进⾏。或者说,系统为每⼀个对象 都创建⼀个SyncList,其实也是可以的,只是内存的消耗会⾮常⼤。
所以,苹果就⽤这个 StripedMap 来解决这个问题,提前准备⼀定个数的 SyncList 放这⾥,然后 在调⽤的时候均匀的进⾏分配。
单向链表
单向链表(Linkedlist)是⼀种常⻅的基础数据结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由⼀连串节点组成
,每个节点保存数据和到下⼀个节点的地址
。单向链表只能向⼀个⽅向遍历。
TLS(Thread Local Storage)
TLS就是线程局部存储,是操作系统为线程单独提供的私有空间,能存储只属于当前线程的⼀些数据
ACQUIRE代表加锁操作,RELEASE代表解锁操作。
@synchronized的底层原理
核⼼图: 整体思路就是:
@synchronize针对某个对象
,也就是我们给@synchronize传的参数
,每⼀条线程都有⼀把递归锁,⽽且记录了每条线程加锁的次数,这样就能通过这俩点,对每条线程⽤不同的递归锁来进⾏加锁和解锁的的操作
,从⽽达到多线程递归调⽤的⽬的。
二. block的类型
block有三种类型:堆block,栈block,全局block。
我们依次来了解它们:
1.首先全局block(NSGlobalBlock):
void (^block)(void) = ^{
};
block();
NSLog(@"%@",block);
**2022-06-16 20:33:30.614794+0800 Block的类型[1520:24575] <__NSGlobalBlock__: 0x10324a050>**
复制代码
block如果没有使⽤外部变量,或者只使⽤静态变量和全局变量,那⼀定是全局blcok。
2.堆block(NSMallocBlock):
int a = 1;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
block();
NSLog(@"%@",block);
**2022-06-16 21:08:53.004907+0800 Block的类型[1819:34534] <__NSMallocBlock__: 0x6000011c12f0>**
复制代码
block如果使⽤了外部变量,⽽且不是静态变量或全局变量,如果赋值给强引⽤的是堆block。
2.栈block(NSStackBlock):
int a = 1;
void (^__weak block)(void) = ^{
NSLog(@"%d",a);
};
block();
NSLog(@"%@",block);
**2022-06-16 21:12:11.795813+0800 Block的类型[1895:37345] <__NSStackBlock__: 0x7ff7bbb78078>**
复制代码
对于这三种类型的block遵循俩个原则:
block如果没有使⽤外部变量
,或者只使⽤静态变量和全局变量,那⼀定是全局blcok。block如果使⽤了外部变量
,⽽且不是静态变量或全局变量,如果赋值给强引⽤的是堆block
,如果赋值给弱引⽤的是栈blcok。
在看一个例子:
- (void)blockDemo1{
NSObject *objc = [NSObject new];
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
void(^block1)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block1();
void(^__weak block2)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block2();
}
复制代码
分析代码:objc这个时候的引用计数是1
,block1是堆block里objc计数+2=3
,block2是栈block里的objc计数+1=4
。 打印结果: 1,3,4 。 堆block为什么是+2呢? 用clang命令生成对应的main.cpp然后找到如下代码:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSObject *__weak weakSelf;//外部变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
// 你写在block代码块里的代码
impl.FuncPtr = fp;
Desc = desc;
}
};
复制代码
看到__main_block_impl_0是一个结构体,block的本质就是一个结构体。看到代码impl.isa = &_NSConcreteStackBlock;
block在创建的时候是一个栈block,哪block是在什么时候变成堆block呢?
运行如上代码,查看汇编:
然后下一个符号断点
objc_retainBlock
: 发现
objc_retainBlock
->_Block_copy
调用了_Block_copy。
进入block源码:
直接上菜了。看到代码:
1.如果block已经在堆上计数+1,返回block。
2.如果block在全局flags & BLOCK_IS_GLOBAL
直接返回block本身。
3.如果block在栈上,需要拷贝block到堆上(重新开辟一个内存地址)result上,最后result->isa = _NSConcreteMallocBlock;
指向了_NSConcreteMallocBlock 堆类型。
为什么block要⽤copy关键字修饰?
因为block在创建的时候,它的内存是分配在栈上的,⽽不是在堆上
。栈区的特点是:对象随时有 可能被销毁,⼀旦被销毁,在调⽤的时候,就会造成系统的崩溃。所以我们要使⽤copy把它拷⻉到堆上。在ARC下, 对于block使⽤copy与strong其实都⼀样, 因为block的retain就是⽤copy来实现的
, 所以在ARC下 block使⽤ copy 和 strong 都可以。
总结:
@synchronized原理:会对参数对象的每条线程,都加一把递归锁,如果能在快速缓存中找到syncCahcheItem
并且当前data与obj相同对item->lockcount
++或--操作。如果没有在快速缓存中找到,先判断快速缓存表有没数据,没有数据直接保存data也就是当前@synchronized的参数到快速缓存中
。快速缓存表中有数据,则把表中的数据copy到线程缓存中,从⽽达到多线程递归调⽤的⽬的。
堆block,栈block,全局block对于这三种类型的block遵循俩个原则:
block如果没有使⽤外部变量
,或者只使⽤静态变量和全局变量,那⼀定是全局blcok。block如果使⽤了外部变量
,⽽且不是静态变量或全局变量,如果赋值给强引⽤的是堆block
,如果赋值给弱引⽤的是栈blcok。
主要探讨了:堆block为什么会从栈block变成堆block呢?在创建堆block的时候,会把当前栈上的block拷贝到堆其中主要的函数是objc_retainBlock
->_Block_copy
。