iOS 底层探究:锁@synchronized

这是我参与8月更文挑战的第20天,活动详情查看:[8月更文挑战]

当我们提到线程时,就会联想到线程不安全,如何确保线程安全以及多线程之间数据访问如何保证不出问题呢?带着这些疑问我们来介绍一下锁的原理。

1 iOS中的锁

1.1 锁的分类

锁分为两大类自旋锁、互斥锁,其中自旋锁是通过忙等(不停询问)实现的,适用于给短小的任务加锁。

  • 自旋锁
    • OSSpinLock
    • dispatch_semaphore_t
    • os_unfair_lock_lock
  • 互斥锁
    • pthread_mutex_t
    • NSLock
    • NSCondition
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized

1.2 各种锁的性能

测试各种锁加锁/解锁10万次的表现

int kRunTimes = 100000;
{
    某锁 lock = 初始化锁;
    double_t beginTime = CFAbsoluteTimeGetCurrent();
    for (int i=0 ; i < kRunTimes; i++) {
        加锁(&lock);
        解锁(&lock);
    }
    double_t endTime = CFAbsoluteTimeGetCurrent() ;
    NSLog(@"某锁: %f ms",(endTime - beginTime)*1000);
}
复制代码

真机iPhoneXR运行结果

OSSpinLock: 1.433015 ms
dispatch_semaphore_t: 2.267957 ms
os_unfair_lock_lock: 2.338052 ms
pthread_mutex_t: 2.584100 ms
NSlock: 2.802968 ms
NSCondition: 2.210975 ms
PTHREAD_MUTEX_RECURSIVE: 2.773046 ms
NSRecursiveLock: 3.018975 ms
NSConditionLock: 5.580902 ms
@synchronized: 9.202957 ms
复制代码

真机iPhone12mini运行结果

OSSpinLock: 0.748038 ms
dispatch_semaphore_t: 1.023054 ms
os_unfair_lock_lock: 0.805020 ms
pthread_mutex_t: 0.934958 ms
NSlock: 1.582980 ms
NSCondition: 1.513004 ms
PTHREAD_MUTEX_RECURSIVE: 2.305984 ms
NSRecursiveLock: 2.532005 ms
NSConditionLock: 8.258939 ms
@synchronized: 3.880978 ms
复制代码

各个锁的运行时间大致可以如下图所示,经过测试,在不同的环境表现不一样,真机表现较好,模拟器表现较差,这说明@synchronized这个锁在真机上有一定的优化。@synchronized锁在我们的应用中使用频率相对较高。

image.png

2 @synchronized的分析

2.1 @synchronized初探

因为@synchronized使用最简单最频繁,所以我们首先研究@synchronized

  • 通过clang转C++代码,查看器底层实现

测试用例

static int a = 0;

int main(int argc, char * argv[]) {
    NSObject *obj = [NSObject alloc];
    @synchronized (obj) {
        a++;
    }
    return 0;
}
复制代码

通过C++代码,我们可以看到几点:

    1. 加锁解锁针对同一对象_sync_obj,通常我们使用self,因为self直接可以用,避免创建额外锁对象,同时在调用对象方法过程中,不会释放,保证锁对象可用。
    1. 加锁通过objc_sync_enter(_sync_obj),解锁通过objc_sync_exit(_sync_obj)@synchronized的锁代码块,放在加锁、解锁之间,起到加锁作用。
static int a = 0;

int main(int argc, char * argv[]) {
    NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
    {
        id _rethrow = 0;
        id _sync_obj = (id)obj;
        objc_sync_enter(_sync_obj);
        try {
            // 包装exit对象
            struct _SYNC_EXIT {
                _SYNC_EXIT(id arg) : sync_exit(arg) {}
                ~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
                id sync_exit;
            } _sync_exit(_sync_obj);
            
            // 加锁代码
            a++;
        } // exit对象析构, ~_SYNC_EXIT() -> objc_sync_exit(_sync_obj)
        catch (id e) {_rethrow = e;}
        { struct _FIN { _FIN(id reth) : rethrow(reth) {}
            ~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
            id rethrow;
        } _fin_force_rethow(_rethrow);}
    }
    
    return 0;
}
复制代码
  • Xcode查看汇编

image.png
我们来探究一下objc_sync_enterobjc_sync_exit内部实现

2.2 @synchronized源码分析

  • objc_sync_enter
// 创建`递归锁`关联`obj`
// 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;
}
复制代码

BREAKPOINT_FUNCTION相当于给一个函数指针添加空实现, BREAKPOINT_FUNCTION( void objc_sync_nil(void) );-> void objc_sync_nil(void) {}.

BREAKPOINT_FUNCTION( void objc_sync_nil(void) );

/* Use this for functions that are intended to be breakpoint hooks.
   If you do not, the compiler may optimize them away.
   BREAKPOINT_FUNCTION( void stop_on_error(void) ); */
#   define BREAKPOINT_FUNCTION(prototype)                             \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }
复制代码

所以说objc_sync_enter(nil),@synchronized(nil) does nothing.

  • objc_sync_exit

同样objc_sync_exit(nil)也是@synchronized(nil) does nothing什么都不做.

// 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;
}
复制代码

与此同时, 我们发现objc_sync_enter,objc_sync_exit惊人相似, 最大的区别是:

  • objc_sync_enter -> SyncData* data = id2data(obj, ACQUIRE)ACQUIRE 获得, 加锁
  • objc_sync_exit -> SyncData* data = id2data(obj, RELEASE)RELEASE 释放, 解锁
  • 说明@synchronized(obj)核心在id2data()的实现, 以及SyncData的数据类型结构.

2.3 @synchronized(object)的核心实现

objc_sync_enterobjc_sync_exit都调用了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;

        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);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            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

    // 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;
            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;
        }
    }

    // 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;
        for (p = *listp; p != NULL; p = p->nextData) {
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // 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;
            result->threadCount = 1;
            goto done;
        }
    }

    // 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;
        }
        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_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;
}
复制代码

代码较复杂,我们一块一块的来分析。
这个函数整个大的代码块

/// 1
if (data) {
 /// 逻辑
}
///21
if (cache) {
/// 逻辑
}
/// 3
lockp->lock();{
/// 逻辑
}
/// 4
lockp->unlock();
    if (result) {
/// 逻辑
}
复制代码

我们接下来,一段一段的来分析。
#if SUPPORT_DIRECT_THREAD_KEYS需要支持TLS,什么是TLS, 我们来解释下。
线程局部存储(Thread Local Storage, TLS)是操作系统为线程单独提供的私有空间,通常只有有限的容量。Linux系统下通常通过pthread库中的
pthread_key_create()、
pthread_getspecific()、
pthread_setspecific()、
pthread_key_delete()

if(data)if(cache) 有两个地方存储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;
复制代码

这段代码对result赋值操作,继续走,

 done:
    lockp->unlock();
复制代码

done了之后,又进行了unlock(),这是为什么呢?
这是因为在内部操作的时候,开辟内存空间,内存空间的相关处理,保证线程安全。
上面我们通过分析SyncData,知道它是一个单向链表,为什么呢,为什么会有两次存储?
spinlock_t lockp = &LOCK_FOR_OBJ(object); 是通过object获取了一把锁,#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock*这是这个宏定义。
*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;
复制代码

这里为什么会有sDataLists
sDataLists是一个静态全局变量。
StripedMap是哈希map,通过下标,哈希有时候会出现冲突,通常我们再次哈希来解决,如果再冲突,我们再次哈希,这是常规做法,这次我们介绍一下拉链法。
sDataLists是静态全局变量,StripedMap其实是哈希结构,也就是全局的哈希表,SyncList的结构如下:

struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};
复制代码

我们通过LLDB调试下,如图:

image.png
这里就是sDataLists结构,一共64个数据。

image.png
第4个数据,说明我们来了一次,这也间接证明这是哈希结构。
synData数据来源于object。
假如我们锁的是self,又来了self锁,又会创建一个synData存储到哈希表中,这个时候关键词都是self,这时候怎么办,这时候会存在哈希冲突的结构,在我们的SyncList不在是一个数据结构,而是通过链表形式存储,因为两两不会冲突,链表结构,不方便查询,但是SyncData是不需要我们查询的,我们只需要加锁,解锁,只需要增删,不需要查询,这就形成了拉链法。

2.4 synchronized的注意事项

  • synchronized锁的对象不要出现空
  • synchronized锁的对象的生命周期至少要跟线程代码的对象一样或者比它长,所以一般我们使用self
  • synchronized锁对象使用self,可以防止多次创建,方便存储和释放。
  • synchronized锁对象使用self,如果当前的对象锁的很长,这个链表就会很大,就会对拉链有一定的负担,不过一般不会操作的很频繁。
  • synchronized锁,模拟器与真机耗时之间相差比较大是因为,在模拟器是有64个大小的限制
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
复制代码

因为这里的判断,如果是真机就是8,64个要耗费长,查的也长时间长。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享