这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战
各种锁真机性能
在我们的认知里,我们是否觉得@synchronized耗费的性能开销最大,在实测中,发现并非如此。
/** OSSpinLock 性能 */
OSSpinLock kc_spinlock = OS_SPINLOCK_INIT;
double_t kc_beginTime = CFAbsoluteTimeGetCurrent();
for (int i=0 ; i < kc_runTimes; i++) {// kc_runTimes = 100000
OSSpinLockLock(&kc_spinlock); //解锁
OSSpinLockUnlock(&kc_spinlock);
}
double_t kc_endTime = CFAbsoluteTimeGetCurrent() ;
KCLog(@"OSSpinLock: %f ms",(kc_endTime - kc_beginTime)*1000);
/** ... */
真机验证的情况如下:
模拟器验证的情况如下:
所以在真机的时候
@synchronized性能不是最差的。我们平时使用的时候使用最广的也是@synchronized
@synchronized原理分析上
@synchronized (self) { }
这里的参数我们平常传入的都是self,那么传入的self意义到底是什么?我们xcrun一下,或者控制台输入clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m此时就得到了一个.cpp文件,打开搜索直接定位到main函数里
首先直接去掉catch里,重点关注try里面的代码块,格式整理下
id _rethrow = 0;
id _sync_obj = (id)appDelegateClassName;
objc_sync_enter(_sync_obj);
try {
struct _SYNC_EXIT {
_SYNC_EXIT(id arg) : sync_exit(arg) {}
~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
id sync_exit;
}
_sync_exit(_sync_obj);
}
这里的_SYNC_EXIT是一个结构体,里面有一个构造函数和一个析构函数。这个结构体可以摘出去到方法外面,此时得到了
objc_sync_enter(_sync_obj);
_sync_exit(_sync_obj);
替换掉析构函数得到:
objc_sync_enter(_sync_obj);
objc_sync_exit(_sync_obj);
此时就得到了@synchronized关键的两个函数。我们在LibObjc源码里搜索下
objc_sync_enter
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;
}
从上面的注释可以看到,如果我们参数传递nil,什么都不会做。
objc_sync_exit
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 = id2data(obj, RELEASE)这一行代码里。点击进去SyncData 可以看出这是一个链表结构。
typedef struct alignas(CacheLineSize) SyncData {
struct SyncData* nextData;
DisguisedPtr<objc_object> object; // 关联对象
int32_t threadCount; // number of THREADS using this block 多线程
recursive_mutex_t mutex; // 递归锁
} SyncData;
SyncList分析
进入函数id2data,发现SyncData从一个叫做sDataLists的全局静态表里面获取,那么这个表是什么?
spinlock_t *lockp = &LOCK_FOR_OBJ(object);
SyncData **listp = &LIST_FOR_OBJ(object);
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;
StripedMap实际上是一个哈希表, 我们可以在这里断点p一下
当obj对象相同的时候,使用拉链法存储SyncData,当加锁的时候就加进去,因为不需要查询,所以使用这样的数据存储结构最巧妙。
@synchronized原理分析下
id2data分析把条件判断先折叠起来
#if SUPPORT_DIRECT_THREAD_KEYS
#endif
这里面的逻辑是一样的,如果有tls就查找tls如果没有就查找Cache缓存。第一次进来的时候是没有data的。首先进来的是这行代码
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;
最后两句说明这个链表是头插法。先创建,之后再存储到表里。下次进来的时候,data有值
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data->object == object)
如果是同一个对象则判断当前的对象锁了多少次,lockCount对应变更。如果当前的lockCount=0表明当前已经解锁完成了,就从当前的缓存中删除;如果不是同一个对象则重新创建一个SyncList
总结:@synchronized最重要的就是两个标识。threadCount来标记当前的对象被多少条线程加锁过,lockCount表明在同一个线程该对象被锁过多少次。这两个标识表明了@synchronized具有多线程可递归性。
@synchronized总结
SyncList是一张哈希表,采取的是拉链法存储syncDatasDatalistarray存储的是synclist(objc)- 底层的函数是
objc_sync_enter/exit对称递归锁 - 采取两种存储:tls/cache
- 第⼀次没有
syncData时,通过头插法链表创建, 标记threadcount=1 - 第二次进入判断是不是同⼀个对象,不是重新创建标记
- 是的话,如果TLS找得到->
lock++ - TLS找不到重新创建一个
syncData并且对threadCount++ - 如果是
exit函数lock--threadCount--
Synchronized:可重⼊ 递归 多线程 锁的对象不要为空
我们平时加self的原因一个是生命周期的管理保证加锁的对象不会意外被释放掉,另一个原因是我们知道syncList的值是objc,在同一个页面使用一个self,只对当前的self拉链,方便存储和释放。另外开头的实验我们发现模拟器和真机的synchronized性能差异比较大
template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
enum { StripeCount = 8 };
#else
enum { StripeCount = 64 };
#endif
如果是真机的话count = 8表明可以有8个线程对比模拟器的话,查的比较快。