iOS 性能差的 @synchronized 有什么优点吗

2,824 阅读7分钟

点赞评论,希望能在你的帮助下越来越好

作者:@iOS成长指北,本文首发于公众号 iOS成长指北,欢迎各位前往指正

如有转载需求请联系我,记住一定要联系哦!!!

前言

ibireme不再安全的 OSSpinLock 一文中,有一张图片简单的比较了各种锁的加解锁性能:

compareLock.png

从上图可知 @synchronized 是 iOS 多线程同步锁中性能最差的一个。但是却是所有锁中使用起来最简单的一个。

一般来说,我们就像下面的示例一样来使用:

@synchronized (self) {
  // code
}

这样就可以保证 {} 中的代码在多线程的情况下线程安全?注意,这里我们有一个? ,如果不合理的使用 @synchronized 同样会导致线程安全问题。

@synchronized 原理

当我们想探究某个方法的底层是怎么实现的,我们可以通过汇编部分来探究这部分代码的具体实现。

我们有两种方法来查看汇编部分

  • Xcode--> Debug -->Debug Workflow --> Always Show Disassembly 显示汇编,然后挂上断点,运行程序
  • Xcode--> Product-->Perform Action --> Assemble **.m 文件

当我们在测试项目中,键入如下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    @synchronized (self) {
        NSLog(@"iOS 成长指北");
    }
}

这里,我们使用第二种方法来查看汇编部分,使用第二种方式有便于我们查找代码的具体位置。当我们搜索 :行数 时,找到具体代码的汇编写法,如同红框中的示例。

synchronized_assemble.png

当我们在调用 NSLog 方法时,存在一个_objc_sync_enter 和两个_objc_sync_exit。由此可知,当代码离开 {} 闭包时,会再执行一次 _objc_sync_exit

萧玉大佬在其《关于 @synchronized,这儿比你想知道的还要多》[1]中说 @synchronized block 会变成 objc_sync_enterobjc_sync_exit 的成对调用。从汇编调用上看,似乎并不是?

当执行 release 方法之后,还会调用一次 objc_sync_exit

源码解析

我们可以查找上述两个方法,最终在 <objc/objc-sync.h> 中找到了_objc_sync_enter_objc_sync_exit。让我们来看看其具体实现

typedef struct SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> 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 创建了一个基于 objkey 的递归互斥的锁 recursive_mutex_t mutex
  • objnil 时,_objc_sync_enter_objc_sync_exit 并不会执行任何操作
  • 我们最终加锁解锁的是 SyncData 结构体,是利用 id2data(obj, usage) 来获取的
  • SyncData 其本质应该是一个链表的头结点,因为使用 nextData 寻找确定对应值

obj 的作用

为什么我们要在使用 @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 来获取对应 objSyncData 和其被加的自旋锁 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<SyncList> sDataLists;

StripedMap 其本质就是一个哈希表,外层是一个数组,数组里的每个位置存储一个类似链表的结构 SyncList

使用哈希表的原因就是为了避免多个obj之间的竞争,其哈希函数是基于obj而不是其他。当我们使用 id2data(obj, usage) 函数获取确定的 SyncData 时,首先先根据hash(obj) 获取对应 SyncList 的头节点SyncData,那么后续做什么呢?

我们看看 id2data(obj, usage) 的其他实现

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;
}
  • 当我们拿到 SyncListSyncData 的头结点时,我们需要查找链表中对应的 SyncData

  • 当存在缓存时,根据是否支持 SUPPORT_DIRECT_THREAD_KEYS ,寻找对应的 SyncData 的方法实现是不同的。一个是根据 tls 另一个是使用**for循环** 来查找。

  • 当没有没有缓存时,我们需要创建对应的缓存。

    • 前面我们说过,SyncList 存在一个自旋锁 spinlock_t lock,其加减锁的时机是在加入缓存的时候实现的,线程缓存找不到任何内容时,会加一个自旋锁。但是 spinlock_t lock 只是一个命名为自旋锁的互斥锁 os_unfair_lock 罢了。
    • 一个值得注意的是,多线程处理时,对应线程可能使用相同的obj 来创建的,但是并没有创建线程缓存,即 SyncData 存在,但是线程缓存不存在。如果 SyncData 不存在,我们需要创建一个对应的SyncData。最后创建 SyncData 的线程缓存,并返回对应的 SyncData ,并加递归互斥锁。

慎用@synchronized(obj)

为什么我们在开头我们说 @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.arraynil 的情况,而 @synchronized (nil) 并不执行加锁解锁操作,会导致线程崩溃。

总结

在所有的线程安全的方案中,@synchronized 以其使用成本成为大部分用户选择,但是性能问题却一直成为他人的诟病。

为什么 @synchronized 是性能最差的呢?因为其包含的操作极为复杂,除了常规的加锁解锁操作以外,还需要考虑哈希表寻址,缓存获取/创建缓存等,最差情况下即 N 个 不同的 obj 创建多个不同的 SyncData,并且会调用命名为自旋锁的互斥锁 os_unfair_lock 来实现缓存。

参考资料

关于 @synchronized,这儿比你想知道的还要多:yulingtianxia.com/blog/2015/1…

IOS - @synchronized详解:www.jianshu.com/p/56f9cfd94…


如果你有任何问题,请直接评论,如果文章有任何不对的地方,请随意表达。如果你愿意,可以通过分享这篇文章来让更多的人发现它。

感谢你阅读本文! 🚀