老青菜

iOS Synchronized 底层分析

2020-03-28

synchronizedobjc中提供的同步锁,支持递归。但是在swift中删除了,可以使用objc_sync替代。

///objc
@synchronized(self) {
    //action
}

///swift
objc_sync_enter(self)
//action
objc_sync_exit(self)

疑问

关于synchronized,可能会有一些疑问,比如:

1. synchronized 的 obj 为 nil 怎么办?
2. synchronized 会对 obj 做什么操作吗?
4. synchronized 和 pthread_mutex 有什么关系?
5. synchronized 和 objc_sync 有什么关系?

想要弄清楚这些问题,还得弄清楚synchronized的底层实现,接下来我们来分析具体的实现。

原理

首先通过汇编来分析下synchronized都做了什么,先写一段测试代码。

//objc
- (void)testSync {
    @synchronized(self) {
    }
}

然后执行xcodeAssemble "OCTest.m",得到以下汇编代码。

Lfunc_begin0:
    //这里省略......
Ltmp0:
    .loc    2 15 5 prologue_end     ## test/OCTest.m:15:5
    callq    *_objc_retain@GOTPCREL(%rip)
    //这里省略......
    callq    _objc_sync_enter
Ltmp1:
    .loc    2 17 5                  ## test/OCTest.m:17:5
    callq    _objc_sync_exit
    //这里省略......
    callq    *_objc_release@GOTPCREL(%rip)

去掉一些多余的代码,我们发现,本质上还是调用了_objc_sync_enter_objc_sync_exit

当然我们也可以用clang来编译当前文件。

clang -x objective-c -rewrite-objc OCTest.m  

打开编译后的OCTest.cpp文件,找到以下c++的代码。

static void _I_OCTest_testSync(OCTest * self, SEL _cmd) {

    { id _rethrow = 0; id _sync_obj = (id)self; 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);


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

}

仔细观察,和汇编分析结果类似,synchronized调用了try catch,内部调用了objc_sync_enterobjc_sync_exit。这两个函数又是如何实现的呢?

objc_sync_enter

带着疑问,我们翻阅objc4源码,在objc-sync.mm文件中,找到了objc_sync_enter的踪影。

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
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;
}

这段源码+注释,很清楚描述了这个函数的作用:
1.在obj上开始同步锁。
2.如果需要,初始化递归互斥锁(recursive mutex),并关联obj
3.obj为nil,加锁不会成功。

objc_sync_exit

同样,在objc-sync.mm文件中,找到了objc_sync_exit的踪影。

// End synchronizing on 'obj'
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

我们再来看看当objc_sync_enter如何加锁的?

//获取obj关联的同步数据
SyncData* data = id2data(obj, ACQUIRE);
//加锁
data->mutex.lock();

SyncData又是什么呢?继续往下找。

//objc-sync.mm
typedef struct SyncData {
     //下一条同步数据
    struct SyncData* nextData;
    //锁的对象
    DisguisedPtr<objc_object> object;
    //等待的线程数量
    int32_t threadCount;  // number of THREADS using this block
    //互斥递归锁
    recursive_mutex_t mutex;
} SyncData;

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

SyncData是一个结构体,类似链表。

  1. nextData:SyncData的指针节点,指向下一条数据
  2. object:锁住的对象
  3. threadCount:等待的线程数量
  4. mutex:使用的互斥递归锁

recursive_mutex_t

recursive_mutex_t是一个互斥递归锁,也是基于pthread_mutex_t的封装。打开objc-os.h找到以下代码:

//objc-os.h
using recursive_mutex_t = recursive_mutex_tt<DEBUG>;
class recursive_mutex_tt : nocopy_t {
    pthread_mutex_t mLock;

  public:
    recursive_mutex_tt() : mLock(PTHREAD_RECURSIVE_MUTEX_INITIALIZER) { }
    void lock() {
        lockdebug_recursive_mutex_lock(this);
        int err = pthread_mutex_lock(&mLock);
        if (err) _objc_fatal("pthread_mutex_lock failed (%d)", err);
    }
    //这里省略......
}

pthread_mutex_t的递归版本,我们就不重复讲解了,具体可以看前面这篇文章

其实到这里,synchronized的原理就很清晰了。

  1. 内部为每一个obj分配一把recursive_mutex递归互斥锁。
  2. 针对每个obj,通过这个recursive_mutex递归互斥锁进行加锁、解锁。

接下来我们来看看内部是如何管理objrecursive_mutex的。

id2data

id2data这一步管理了objSyncData的映射关系,根据obj获取SyncData,主要分为五步。

tls

第一步,从当前线程的Thread Local Storage快速缓存中获取SyncData,只适合一个线程一应一个SyncData

static SyncData* id2data(id object, enum usage why) {
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

    //1.先从线程Thread Local Storage快速缓存中获取 SyncData
    #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) {
            //获取当前线程tls缓存里的SyncData加锁次数
          lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
           //判断当前操作类型
          switch(why) {
              //获取锁
              case ACQUIRE: {
                    //加锁一次,更新当前线程tls缓存
                    lockCount++;
                    tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                    break;
              }
              //释放锁
              case RELEASE:
                  //释放锁一次,更新当前线程tls缓存
                    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;
              //这里省略部分代码.....
            }
            return result;
        }
    }
#endif

fetch_cache

第二步,从当前线程缓存中获取SyncCache结构体,包含了SyncCacheItem数组,一个线程可以对应多个SyncCacheItem同步对象,也就是一个线程可以处理多个SyncData

//从当前线程缓存中获取SyncCache
//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++) {
         //编译SyncCache SyncCacheItem列表。
        SyncCacheItem *item = &cache->list[i];
        if (item->data->object != object) continue;
        // Found a match.
        //找到匹配当前 obj 的 SyncCacheItem
        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;
        //这里省略部分代码......
        return result;
    }
}

LIST_FOR_OBJ

第三步,通过obj在全局哈希表sDataLists中查找SyncData列表,因为sDataLists是全局共享的,所以这里使用了spinlock_t加锁。

// 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;
    //遍历SyncData列表
    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;
        }
        //标记未使用的SyncData
        if ( (firstUnused == NULL) && (p->threadCount == 0) )
            firstUnused = p;
    }
    // no SyncData currently associated with object
    //没有找到SyncData
    if ( (why == RELEASE) || (why == CHECK) )
        goto done;
    // an unused one was found, use it
    //找到SyncData,且未使用,重复利用
    if ( firstUnused != NULL ) {
        result = firstUnused;
        result->object = (objc_object *)object;
        result->threadCount = 1;
        goto done;
    }
}

New SyncData

第四步,如果上面三步都没有找到SyncData,那么需要新建SyncData

// 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.
result = (SyncData*)calloc(sizeof(SyncData), 1);
result->object = (objc_object *)object;
result->threadCount = 1;
//new 递归互斥锁
new (&result->mutex) recursive_mutex_t();
result->nextData = *listp;
*listp = result;

Save SyncData

第五步,保存SyncData对象。

done:
     //释放sDataLists的锁
    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.
        //ACQUIRE需要保存SyncData
        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");
          //线程tls fast cache模式,缓存SyncData
#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
             //SyncCache模式,加入到SyncCacheItem数组
        {
            // Save in thread cache
            if (!cache) cache = fetch_cache(YES);
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

至此,id2data的功能已经大致清晰。利用SyncData管理obj、线程、递归互斥锁之间的关系。

  1. 先从当前线程的tls fast cache快速缓存中去获取单个SyncData对象。
  2. 如果1中SyncData未找到,再从当前线程的缓存中获取SyncCache,遍历SyncCacheItem数组,找到对应的SyncData
  3. 如果2中SyncData未找到,再从全局的哈希表sDataLists中查找SyncCache,查看其它线程是否已经占用过obj
  4. 如果还是没有找到SyncData,则新建一个SyncData对象。
  5. 把新建的SyncData加入到当前线程缓存里,或者全局的哈希表sDataLists中。

回顾

相信看到这里,开头的这些问题,我们应该都知道答案了吧。

1. synchronized 的 obj 为 nil 怎么办?
    加锁操作无效。

2. synchronized 会对 obj 做什么操作吗?
    会为obj生成递归自旋锁,并建立关联,生成 SyncData,存储在当前线程的缓存里或者全局哈希表里。

4. synchronized 和 pthread_mutex 有什么关系?
    SyncData里的递归互斥锁,使用 pthread_mutex 实现的。

5. synchronized 和 objc_sync 有什么关系?
    synchronized 底层调用了 objc_sync_enter() 和 objc_sync_exit()

The Next

  1. iOS 中常用的锁
  2. iOS OSSpinLock
  3. iOS Synchronized
  4. iOS NSLock那些琐
  5. iOS Atomic 底层分析

参考链接

swift foundation
opensource objc4

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章