iOS八股文(十五)@synchronized为何如此强大?

442 阅读8分钟

前面的文章把iOS中能用的锁的讲解了一遍,其中@synchronized是一把非常强大的锁,支持多线程的递归调用,而在使用的时候只要把代码写进括号内,不需要管理锁的加锁和解锁,非常方便。这也是其倍受青睐的原因。本文就一层一层揭开@synchronized的神秘面纱,探究synchronized的工作原理。

@synchronized源码入口

首先看看@synchronized的使用:

int object_c_source_m() {
    NSObject *obj1 = [[NSObject alloc] init];
    @synchronized (obj1) {
        // Code 。。。
    }
    return 0;
}

可以使用clang -rewrit-objc,将代码重写为c++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OSSynchornizedOriginFile.m -o OSSynchCPPForIphone.cpp

可以找到@synchronized对应的代码如下,为了方便阅读,这里将关键代码进行了提取。

    id _sync_obj = (id)obj1;
     objc_sync_enter(_sync_obj);
     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(_sync_obj)是调用_SYNC_EXIT的构造函数,而~_SYNC_EXIT()_SYNC_EXIT的析构函数,构造出的实例没有使用,在回收的时候就会调用析构函数。所以根本调用的还是objc_sync_exit(sync_exit)

@synchronized在底层其实是分别调用了objc_sync_enterobjc_sync_exit

enter、exit函数

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

从源码中我们可以获得2个有用的信息:

  • 如果obj为nil,相当于什么都没有做
  • 需要进一步了解SyncData的数据结构,@synchronized中也是通过互斥锁(data->mutex)实现的。

数据结构

在分析正式源码之前,必须要知道其中的部分数据结构的定义,这对流程理解非常重要。

SyncData

源码定义:

//alignas  关键字用来声明对齐字节的
typedef struct alignas(CacheLineSize) SyncData {
    //链表结构
    struct SyncData* nextData;
    //@synchronized中传入的对象
    DisguisedPtr<objc_object> object;
    // 多少个线程使用
    int32_t threadCount;  // number of THREADS using this block
    // 一把递归锁
    recursive_mutex_t mutex;

} SyncData;

可以通过注解SyncData其实是一个单向链表结构,其中存了我们@synchronized中传入的对象,还有一把锁,还有线程数量。

关于这个锁可以继续看其定义:

using recursive_mutex_t = recursive_mutex_tt<LOCKDEBUG>;
class recursive_mutex_tt : nocopy_t {
    os_unfair_recursive_lock mLock;
    //code ...
    }
/*!
 * @typedef os_unfair_recursive_lock
 *
 * @abstract
 * Low-level lock that allows waiters to block efficiently on contention.
 *
 * @discussion
 * See os_unfair_lock.
 *
 */
OS_UNFAIR_RECURSIVE_LOCK_AVAILABILITY
typedef struct os_unfair_recursive_lock_s {
	os_unfair_lock ourl_lock;
	uint32_t ourl_count;
} os_unfair_recursive_lock, *os_unfair_recursive_lock_t;

可以看到,其根本是基于os_unfair_lock的封装。在之前的版本,这个是基于pthread_mutex_t的封装。

SyncCache

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;

顾名思义,SyncCacheSyncData的缓存。可以简易的画出其结构图:

image.png

Fast cache

/*
  快缓存。 :  2个固定的线程键  储存一个单独的 SyncCacheItem
  Fast cache: two fixed pthread keys store a single SyncCacheItem. 
  这就避免了对于一次只同步单个对象的线程使用SyncCache的malloc
  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
 */

这一段虽然全是注释,但是非常重要,这里引出了一个关键的概念-Fast cache。这个跟上面的缓存不一样,上面的缓存是存了一个列表,而Fast cache只存了单个的SyncCacheItem用两个key来获取其成员变量datalockCount

sDataLists

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

sDataLists 为全局静态,全局只有一份。可以简单的理解为一张hash Map

struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

// StripedMap<T> is a map of void* -> T, sized appropriately 
// 作用是友好的缓存锁
// for cache-friendly lock striping. 
// 可以直接缓存spinlock_t,也可缓存其他内部含有spin lock 的结构体
// For example, this may be used as StripedMap<spinlock_t>
// or as StripedMap<SomeStruct> where SomeStruct stores a spin lock.
template<typename T>
class StripedMap {
//真机状态下
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
//其他
#else
    enum { StripeCount = 64 };
#endif
   // code ...
   }

表的大小跟架构环境和是否是真机有关,可以将sDataLists的结构总结如下:

image.png

id2data

有了上面这些数据结构的基础,我们再来看id2data这个方法就会比较轻松了。

那先看id2data的实现:

image.png 这里我把id2data这小二百行代码分为5个步骤。

Fast cache 查找

#if SUPPORT_DIRECT_THREAD_KEYS
    // 1⃣️去快速缓存里面找
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    //拿出快速缓存里面的SyncData
    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) {
            //加锁的时候(ENTER)
            case ACQUIRE: {
                lockCount++;
                //lockCount放入快速缓存
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                break;
            }
            //解锁的时候(EXIT)
            case RELEASE:
                lockCount--;
                //取出加锁的时候的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
                    // SyncData中记录线程数量的-1
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }
            //直接返回
            return result;
        }
    }
#endif

注意⚠️其中TLS(thread Local Store)为线程本地存储。也就是说每条线程都会有一个这样的FastCache。并不是整个过程只有一个FastCache。如果在FastCache找到就直接返回。

syncCache 查找

{
    // 2⃣️在线程的TLS中找objc对象,然后再维护2个count lockCount 和 threadCount
    // 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--;
                //如果==0,该线程已经使用完了
                if (item->lockCount == 0) {
                    // remove from per-thread cache
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    // threadCount -1。防止和加锁的时候通途
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }
            //返回
            return result;
        }
    }
}

这里同样是在缓存找,因为SyncCache里面是数组,这里遍历查找。可以看其中fetch_cache(NO)中的代码:

static SyncCache *fetch_cache(bool create)
{
    _objc_pthread_data *data;
    
    data = _objc_fetch_pthread_data(create);
    if (!data) return NULL;

    if (!data->syncCache) {
        if (!create) {
            return NULL;
        } else {
            int count = 4;
            data->syncCache = (SyncCache *)
                calloc(1, sizeof(SyncCache) + count*sizeof(SyncCacheItem));
            data->syncCache->allocated = count;
        }
    }

    // Make sure there's at least one open slot in the list.
    if (data->syncCache->allocated == data->syncCache->used) {
        data->syncCache->allocated *= 2;
        data->syncCache = (SyncCache *)
            realloc(data->syncCache, sizeof(SyncCache) 
                    + data->syncCache->allocated * sizeof(SyncCacheItem));
    }

    return data->syncCache;
}

可以大致看一眼_objc_pthread_data的数据结构:

   //每一个线程的存储
// objc per-thread storage
typedef struct {
    struct _objc_initializing_classes *initializingClasses; // for +initialize
    // ⚠️注释!!!
    struct SyncCache *syncCache;  // for @synchronize
    struct alt_handler_list *handlerList;  // for exception alt handlers
    char *printableNames[4];  // temporary demangled names for logging
    const char **classNameLookups;  // for objc_getClass() hooks
    unsigned classNameLookupsAllocated;
    unsigned classNameLookupsUsed;

    // If you add new fields here, don't forget to update 
    // _objc_pthread_destroyspecific()

} _objc_pthread_data;

这里也进一步说明了TLSsyncCache也是每个线程中都存在一份的。

sDataLists 查找

如果在快速缓存和缓存里面都没有找到,这时候是这个线程第一次走到 @synchronized的地方,系统会去sDataLists里面去找对应的SyncData对象:

{
    // 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.
    
    // 这里加锁内容包括sDataLists查找,和创建SyncData,目的是为了防止创建重复的和创建SyncData
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        //遍历链表
            for (p = *listp; p != NULL; p = p->nextData) {
            //找到SyncData
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                // threadCount + 1
                OSAtomicIncrement32Barrier(&result->threadCount);
                // 跳转:done
                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;
        }
    }
}

新建SyncData

{
 // 4⃣️再找不到,只能创建
    // 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;
}
  • 在快速缓存中没有找到
  • 在线程缓存中也没有找到
  • 在全局的sDataLists中也没有找到
  • 那就自己动手丰衣足食,自己新建一个。

缓存到线程中

   // 5⃣️缓存
 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;
}

这里只会在Enter的时候执行,如果支持快速缓存并且快速缓存里面没有值,那么在快速缓存里面去添加,方便下次递归的时候来加锁。否则就在线程缓存里面添加。

id2data在找锁的过程中使用了类似三级缓存的流程,这样的目的是为了在多线程中管理锁,并且让线程以最快的速度拿到锁,来完成加锁解锁的操作,从而提升效率。

总结

  • @synchronized使用时如果传入nil,不能完成加锁,使用时应避免。
  • @synchronized使用了快速缓存、线程缓存、全局链表方式来使线程更快的拿到锁,以提升效率。
  • @synchronized内部是基于os_unfair_lock封装的递归互斥锁。
  • @synchronized内部在创建锁的时候为了唯一性,使用到spinlock_t(基于os_unfair_lock封装的)来确保线程安全。