锁的性能排行
锁的归类
自旋锁:线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显示释放自旋锁。自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
互斥锁:是一种用于多线程编程中,防止两条线程同时对同一公共资源进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
上图中属于互斥锁的有:
- NSLock
- pthread_mutex
- @synchronized
条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源分配到了,条件锁打开,进程继续运行
上图中属于条件锁的有:
- NSConfition
- NSConditionLock
递归锁:就是同一线程可以加锁N次而不会引发死锁
上图中属于递归锁的有
- NSRecursiveLock
- pthread_mutex(recursive)
信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间的互斥。
其实基本的锁就包括了三类,自旋锁 互斥锁 读写锁,其他的比如条件锁,递归锁,信号量都是上层的封装和实现!
引用:百度百科读写锁
@synchronized
对于@synchronized 的使用大家都不陌生,但是它的底层实现是怎样的呢?通过底层分析我们又能得到什么新的发现?下面废话不多说直接探寻其底层。
如何进行探索(知道的可略过直接去看底层源码分析)
1、 dome 准备
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.ticketCount = 15;
[self testSaleTicket];
}
- (void)testSaleTicket{
///窗口 1
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
///窗口 2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
///窗口 3
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
// @synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
// }
}
在没有考虑到线程安全的情况我们运行其任务
- 这明显这票数 有问题 票池 抽了疯,不管三七二一的瞎胡 扯的反馈。
当用上了 @synchronized 完美的解决了问题
2、如何分析synchronize
那肯定是符号断点,clang了
首先符号断点打开 Debug
-> Debug Workflow
-> Always Show Disassembly
将断点 打到 @synchronized 并运行 在汇编里我们找到了 两个很重要的线索
- objc_sync_enter 函数
- objc_sync_exit 函数
我们 到此先记住这两个函 这是可疑的两个函数 下面在clang一下 @synchronized 看clang编译器是怎样实现的。
在main函数中写一个 @synchronized
通过命令 得到 mian.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-14.0.0 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.0.sdk main.m
- 通过 clang我们 也发现了上面的两个函数 是一模一样的,证明上面的两个函数 正是 我们要研究的。
找到 objc_sync_enter 和 objc_sync_exit 所在的库
下符号断点
-
此时我们知道了 objc_sync_enter 函数 是在 libojc 中掉起的。
objc_sync_exit 函数 也是由 libobjc 中调起的
到这里我们也就知道了@synchronized 底层 是由 objc_sync_enter 和objc_sync_exit 两个重要的函数组合而成 他们来自 libobjc 动态库。也就找到 程序的入口 分析的入口。
objc_sync_enter&objc_sync_exit 函数分析
找到objc4源码 并定位到当前函数
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;
}
- 从这里可以看到 如果obj为真的话 通过
id2data
函数 获取一个SyncData
对象,并将此对象里面的mutex
的属性 上锁
我们看 SyncData 类型
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
- 可以看到
SyncData
是一个结构体,里面包含一个指向下一个SyncData
的指针nextData
,可以看出SyncData
是链表中的一个节点。 - 包含
object
将其类型进行了伪装,其实它就是我们传进来的object
。 - 里面还有一个
threadCount
,通过注释我们可以详细的看到 使用此块的线程数。 - 还有一把锁,从这把锁的定义来看 它是一个
递归互斥
类型
来到 id2data函数
看里面如何获取到SyncData
的对象的
由于函数太长我们拆分几大块来看
第一步: 判断是否支持tls
缓存,从tls
缓存中获取obj的相关信息
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;
///从线程中读取数据 (tls: (Thread Local Storage) 线程本地存储)
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
/// 找到了 设置为 YES
fastCacheOccupied = YES;
///如对象是传入的对象
if (data->object == object) {
// Found a match in fast cache. ///从快速缓存中找到
uintptr_t lockCount;
///返回值赋值
result = data;
/// 当前线程 被锁了 几回 如当前线程递归调用锁
lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
/// 如果使用此块儿的线程总数 或者 当前线程被锁次数 都小于等于0 那么这时候bug
if (result->threadCount <= 0 || lockCount <= 0) {
_objc_fatal("id2data fastcache is buggy");
}
switch(why) {
case ACQUIRE: {///进行中
lockCount++;///将当前线程被锁次数+1
///更新线程缓存的任务数
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:/// 释放中
lockCount--;///将当前线程被锁次数 -1
///更新线程缓存的任务数
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
/// 如当前线程被锁的任务都执行完了 那么 释放线程缓存
if (lockCount == 0) {
// remove from fast cache
tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:///啥都不干 应该是预留
// do nothing
break;
}
/// 返回
return result;
}
}
#endif
tls,Thread Local Storage,线程局部储存,它是操作系统为线程单独提供的私有空间,通常只有有限的容量。
百度百科:线程局部存储
- 从
tls
读取数据,如果找到了并且和当前被锁对象一样,获取当前 线程 被锁几回的lockCount
。 - 如当前 是
ACQUIRE
(也就是objc_sync_enter
调用的)那说明在当前线程上对象又被锁了一次,锁的次数加+1。 更新tls
中存储的obj信息。并返回 - 如当前 是
RELEASE
(也就是objc_sync_exit
)发起的调用,那说明 在当前线程上的被锁任务应该 -1 。更新tls
中存储的obj信息。并返回 - 如在
tls
中并未找到,那么进入第二步
第二步:在线程缓存中SyncCache
中查找是否存在obj的数据信息
#endif
/// //检查已拥有锁的每个线程缓存是否匹配对象
// Check per-thread cache of already-owned locks for matching object
SyncCache *cache = fetch_cache(NO);
if (cache) {
unsigned int i;
///遍历所有的拥有锁任务的线程 在线程缓存中
for (i = 0; i < cache->used; i++) {
SyncCacheItem *item = &cache->list[i];
///判断线程中的对象并不是我们传进的对象 跳过本次循环
if (item->data->object != object) continue;
// Found a match. ///找到了当前对象所关联的线程。
result = item->data;
/// 如果 当前对象所关联的 线程总数 小于等于0
/// 或 当前对象所关联的线程 锁任务的个数小于等于0 程序bug
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE: ///进行中
item->lockCount++; ///当前线程任务数+1
break;
case RELEASE:///释放中
item->lockCount--; ///当前线程任务数 -1
if (item->lockCount == 0) { ///当前线程加锁任务 为 0 那么 移除缓存
// remove from per-thread cache
cache->list[i] = cache->list[--cache->used];
// atomic because may collide with concurrent ACQUIRE
OSAtomicDecrement32Barrier(&result->threadCount);
}
break;
case CHECK:
// do nothing ///啥都不干
break;
}
return result; ///返回
}
}
从线程缓存中遍历查找 和当前传进的对象对应的线程缓存。 如找到了 拿到 当前线程的缓存对象
SyncCacheItem
。如当前 是
ACQUIRE
(也就是objc_sync_enter
调用的)那说明在当前线程上对象又被锁了一次,锁的次数(lockCount
)加+1。如当前 是
RELEASE
(也就是objc_sync_exit
)发起的调用,那说明 在当前线程上的被锁任务次数标识(lockCount
)应该 -1 。 如果当前线程上的任务数为0 那么移除线程缓存如在线程缓存中也没有那么进入第三步
这里看一下 缓存结构(
SyncCache
)及 缓存对象结构(SyncCacheItem
)///线程缓存 typedef struct SyncCache { unsigned int allocated; unsigned int used; SyncCacheItem list[0]; } SyncCache; ///缓存对象item typedef struct { SyncData *data; unsigned int lockCount; // number of times THIS THREAD locked >this block } SyncCacheItem;
第三步:使用列表 sDataLists
中查找对象,并做处理
// 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.
///线程缓存没有找到任何东西。,需要遍历每个线程,沿着nextData递归查找
///上锁
lockp->lock();
{
SyncData* p;
SyncData* firstUnused = NULL;
///遍历跟当前object相关的 所有线程任务
for (p = *listp; p != NULL; p = p->nextData) {
///再次判断是否是当前 object
if ( p->object == object ) {
result = p;//找到赋值
//原子操作 可能会和 并发 释放 冲突
// atomic because may collide with concurrent RELEASE
OSAtomicIncrement32Barrier(&result->threadCount);
goto done;//跳出
}
///没找到与当前objc关联的锁任务线程 更新第一个没有使用的线程
if ( (firstUnused == NULL) && (p->threadCount == 0) )
firstUnused = p;
}
//当前没有与对象关联的SyncData
// no SyncData currently associated with object
if ( (why == RELEASE) || (why == CHECK) )
goto done;
//发现一个未使用的,就使用它
// an unused one was found, use it
if ( firstUnused != NULL ) {
result = firstUnused;
result->object = (objc_object *)object;///将当前对象存入 object
result->threadCount = 1;//只有一个线程加锁
goto done;
}
}
//分配一个新的SyncData并添加到列表
// 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.
//分配一个新的SyncData并添加到列表。
// XXX用持有的全局锁分配内存是不好的做法,
//可能值得释放锁、重新分配和搜索。
//但由于我们从来没有释放这些,我们就不会经常陷入分配的困境。
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;
}
if (why != ACQUIRE) _objc_fatal("id2data is buggy");
if (result->object != object) _objc_fatal("id2data is buggy");
#if SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache ///存入 tls
tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
} else
#endif
{
// Save in thread cache //存入线程缓存
if (!cache) cache = fetch_cache(YES);
cache->list[cache->used].data = result;
cache->list[cache->used].lockCount = 1;
cache->used++;
}
}
return result;
}
- 在列表
sDataLists
中 查找,就需要对查找过程加锁防止多线程查找导致数据异常。使用列表sDataLists
把SyncData
又做了一层封装,元素是一个结构体SyncList
.
这里我们回到最上面看一下 *listp
///跟当前对象关联的所有的被锁线程中的锁任务的状态 spinlock_t *lockp = &LOCK_FOR_OBJ(object); ///跟当前对象关联的所有的被锁线程数据 SyncData **listp = &LIST_FOR_OBJ(object); ///进入 LOCK_FOR_OBJ 发现是一个宏 #define LOCK_FOR_OBJ(obj) sDataLists[obj].lock #define LIST_FOR_OBJ(obj) sDataLists[obj].data ///sDataLists 是一个静态的 map 泛型为 SyncList 也就是key为object指针,value为SynLlist static StripedMap
sDataLists; struct SyncList { SyncData *data; spinlock_t lock; constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { } };
- 如找到,解锁,将数据写入
tls
,写入线程缓存
,并返回数据 - 如未找到,创建一个新的
SyncData
放入sDataLists
中,并存入tls
和线程缓存
中然后返回
看完了objc_sync_enter
下面看 objc_sync_exit
锁的释放
// End synchronizing on 'obj'. ///根据obj结束 加锁
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
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;
}
上面我们已经统一的分析了id2data
函数,这里传进的是RELEASE
下面总结objc_sync_exit 函数 的id2data做了什么事情
1、先从
tls
缓存中查找,如果找到,对锁的计数(lockCount
)减1,更新缓存中的数据,如果当前对象对应的锁计数为0了,直接将其从tls
缓存中删除。未找到进入22、从线程缓存
SyncCache
中查找,如果找到,对锁的计数减1
,更新缓存中的数据,如果当前对象对应的锁计数为0了,直接将其从线程缓存SyncCache
中删除。未找到进入33、从
sDataLists
查找,找到的话,直接将其置为nil
。
总结
-
synchronized
底层我们看到了有对同一条线程上的 加锁任务计数lockCount
。有 使用此块 的线程数的统计threadCount
还看到了SyncData
对象中 的recursive_mutex_t
。
由此可以下结论synchronized
是一把递归互斥锁
, -
synchronized
进入代码块的入口为objc_sync_enter
,出口为objc_sync_enter
- 如果
@synchronized(nil)
传入的为nil那么锁将不起任何作用
核心处理逻辑: - 如支持tls缓存,就从tls缓存中查找对象
SyncData
,找到对lockCount
进行相应操作。 - 如果不支持
tls
缓存,或者从tls
缓存中未找到,就从线程缓存SyncCache
中查找,同样如找到 就对lockCount
进行相应操作。 - 如缓存中没有找到,就从
sDataLists链表
中查找,找到后进行相关操作,并写入tls
缓存和线程缓存SyncCache
. - 都没找到,创建一个节点,将对象锁
SyncData
插入sDataLists
,并写入缓存.