在 ibireme 的 不再安全的 OSSpinLock 一文中,有一张图片简单地比较了各种锁的加锁解锁性能:
图片来源于 http://blog.ibireme.com/
从上图可知 「@synchronized」 是 iOS 多线程同步锁中性能最差的一个。但是却是所有锁中使用起来最简单的一个。
一般来说,我们就像下面的示例一样来使用:
@synchronized (self) {
// code
}
这样就可以保证 {} 中的代码在多线程的情况下线程安全?注意,这里我们有一个? ,如果不合理地使用 「@synchronized」 同样会导致线程安全问题。
当我们想探究某个方法的底层是怎么实现的,我们可以通过汇编部分来探究这部分代码的具体实现。
我们有两种方法来查看汇编部分
当我们在测试项目中,键入如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
@synchronized (self) {
NSLog(@"iOS 成长指北");
}
}
我们利用第二种方法来查看汇编部分,使用第二种方式有便于我们查找代码的「具体位置」。当我们搜索 :行数 时,找到具体代码的汇编写法,如同红框中的示例。
当我们在调用 NSLog 方法时,存在一个_objc_sync_enter 和两个_objc_sync_exit。由此可知,当代码离开 {} 闭包时,会再执行一次 _objc_sync_exit。
萧玉大佬在其《关于 @synchronized,这儿比你想知道的还要多》[1]中说 @synchronized block 会变成 objc_sync_enter 和 objc_sync_exit 的成对调用。从汇编调用上看,似乎并不是?
当执行 release 方法之后,还会「调用一次」 objc_sync_exit。
我们可以查找上述两个方法,最终在
typedef struct SyncData {
struct SyncData* nextData;
DisguisedPtr object;
int32_t threadCount; // number of THREADS using this block
recursive_mutex_t mutex;
} SyncData;
// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.
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;
}
// End synchronizing on '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;
}
从源代码和注释中,我们可以发现:
为什么我们要在使用 「@synchronized」 的时候,我们需要传一个obj 呢?我们看一下使用的 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;
...
}
当我们使用时,通过 StripedMap 来获取对应 obj 的 SyncData 和其被加的自旋锁 spinlock_t。
/*
Fast cache: two fixed pthread keys store a single SyncCacheItem.
This avoids malloc of the SyncCache for threads that only synchronize
a single object at a time.
SYNC_DATA_DIRECT_KEY == SyncCacheItem.data
SYNC_COUNT_DIRECT_KEY == SyncCacheItem.lockCount
*/
struct SyncList {
SyncData *data;
spinlock_t lock;
SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
// Use multiple parallel lists to decrease contention among unrelated objects.
// 使用多个并行列表可以减少不相关对象之间的争用。
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap sDataLists;
StripedMap 其本质就是一个哈希表,外层是一个数组,数组里的每个位置存储一个类似链表的结构 SyncList。
使用哈希表的原因就是为了避免多个obj之间的竞争,其哈希函数是基于obj而不是其他。当我们使用 id2data(obj, usage) 函数获取确定的 SyncData 时,首先先根据hash(obj) 获取对应 SyncList 的头节点SyncData,那么后续做什么呢?
我们看看 id2data(obj, usage) 的其他实现
如果我们要了解具体如何获取到,我们需要查看
static SyncData* id2data(id object, enum usage why)
{
...
#if SUPPORT_DIRECT_THREAD_KEYS
// 检查线程 Fast Cache 中是否有匹配的对象
bool fastCacheOccupied = NO;
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
if (data) {
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);
switch(why) {
case ACQUIRE: {
// 加锁操作
lockCount++;
tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
break;
}
case RELEASE:
//解锁操作
lockCount--;
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
// 当我们没有从线程快速缓存中获取 SyncData 我们需要从拥有锁的线程缓存中获取
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;
if (result->threadCount <= 0 || item->lockCount <= 0) {
_objc_fatal("id2data cache is buggy");
}
switch(why) {
case ACQUIRE:
item->lockCount++;
break;
case RELEASE:
item->lockCount--;
if (item->lockCount == 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;
}
}
lockp->lock();
{
...
//创建缓存,会根据当前的缓存类型来判断是存入到那种线程缓存中
goto done;
}
// malloc a new SyncData and add to list.
// XXX calling malloc with a global lock held is bad practice,
// might be worth releasing the lock, mallocing, and searching again.
// But since we never free these guys we won't be stuck in malloc very often.
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 SUPPORT_DIRECT_THREAD_KEYS
if (!fastCacheOccupied) {
// Save in fast thread cache
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;
}
为什么我们在开头我们说 「@synchronized」 并不能保证线程安全,当我们使用一个可能变成 nil的对象作为 obj 时,会发生线程安全问题。
for (NSInteger i = 0; i < 10000; i ++) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@synchronized (self.array) {
self.array = [NSMutableArray array];
}
});
}
这个例子来自于参考资料[2],稍微修改了一下创建的次数,如果是真机调试可能需要更少的调试次数,模拟器的话其支持的次数会比较多一点。
这个例子会发生崩溃,是因为ARC下 setArray: 的方法会执行一个 release 操作,在某个线程中会出现 self.array 为 nil 的情况,而 @synchronized (nil) 并不执行「加锁解锁」操作,会导致线程崩溃。
在所有的线程安全的方案中,**@synchronized** 以其「使用成本」成为大部分用户选择。但是其性能问题却一直成为他人的诟病。
为什么 「@synchronized」 是性能最差的呢?因为其包含的操作极为复杂,除了常规的加锁解锁操作以外,还需要考虑哈希表寻址,缓存获取/创建缓存等,最差情况下即 N 个 不同的 obj 创建多个不同的 SyncData,并且会调用自旋锁来实现缓存。
关于 @synchronized,这儿比你想知道的还要多:http://yulingtianxia.com/blog/2015/11/01/More-than-you-want-to-know-about-synchronized/
IOS - @synchronized详解:https://www.jianshu.com/p/56f9cfd94146
留言与评论(共有 0 条评论) “” |