@synchronized底层原理

知 识 点 / 超 人


@synchronized是一种对对象加锁方式,跟互斥锁类似。当你需要在多线程环境下控制某个对象的处理代码按照队列方式一个一个执行的时候,就可以用@synchronized来对对象进行加锁,通过@synchronized自动加锁解锁来完成对代码的执行顺序控制。

    NSString * appDelegateClassName;
    @synchronized (appDelegateClassName) {
        NSLog(@"class name :%@",appDelegateClassName);
    }

通过xcrun iphonesimulator clang -rewrite-objc main.m 命令查看C/C++源码 得知上面的代码在编译器中实际是

NSString * appDelegateClassName;
  {
        id _rethrow = 0;//从rethrow在代码块里的使用来看,它是一个接收try catch抛出异常的变量。

      //为什么这里要把已经定义好的NSString对象强转为id类型呢?
      //并且后面的代码块没有任何地方使用appDelegateClassName,而是使用的强转后的_sync_obj。
      //这里大概猜测是为了避免appDelegateClassName变为nil。导致内部代码块出异常
        id _sync_obj = (id)appDelegateClassName;

        objc_sync_enter(_sync_obj);//通过该函数进行加锁操作。后面会详细对这个函数说明
        try {//这里使用了 try catch语法,说明其中的执行可能会发生异常。具体可能会发生什么异常呢?后面会说明
            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_EXIT去执行objc_sync_exit(sync_exit);

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_ff_d1zlzd014bl3bx8zyzqkyk680000gn_T_main_f068e0_mi_0,appDelegateClassName);
        } catch (id e) {
            _rethrow = e;//保留异常
        }
        { struct _FIN
            {
                _FIN(id reth) : rethrow(reth) {}
                ~_FIN() {
                    if (rethrow) objc_exception_throw(rethrow);//当rethrow有异常值时,则抛出异常。
                }
                id rethrow;
            } _fin_force_rethow(_rethrow);
          //上面定义了一个_FIN的结构体
          //并定义了一个带参赋值的构造函数和一个析构函数 ~_FIN去执行if (rethrow) objc_exception_throw(rethrow);
        }
    }

看了上面的代码,给我留下了3个疑问
1.objc_sync_enter与objc_sync_exit函数的作用是什么
2.为什么要强转appDelegateClassName
3.为什么定义这两个结构体

objc_sync_enter与objc_sync_exit函数的作用是什么
objc_sync_enter与objc_sync_eixt函数在<objc/objc-sync.h>中,是runtime的类,可以直接查看源码

先看objc-sync的上半部分。

typedef struct SyncData {
    struct SyncData* nextData; // 指向下一个同步对象
    DisguisedPtr<objc_object> object; // 指向需要加锁的对象
    int32_t threadCount;  // 标记 加锁的object对象 被加锁的次数
    recursive_mutex_t mutex; // 互斥锁
} SyncData;

typedef struct {
    SyncData *data; // 同步对象
    unsigned int lockCount;  // 对同步对象中的object对象加锁的次数
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated; // list 数组 中 SyncCacheItem对象的总个数
    unsigned int used; // list 数组 中 已经执行完成的个数
    SyncCacheItem list[0]; // SyncCacheItem的数组
} SyncCache;

/*
  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) { } //构造函数
};

// Use multiple parallel lists to decrease contention among unrelated objects.
#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock  //获得obj对象的锁
#define LIST_FOR_OBJ(obj) sDataLists[obj].data    //获得obj对象的数据
static StripedMap<SyncList> sDataLists;// 定义StripedMap<SyncList>类型的数组,sDataLists是一个全局StripedMap哈希列表,其中value为SyncList对象,key为加锁对象object指针进行hash后的值。



enum usage { ACQUIRE, RELEASE, CHECK };//三个操作的标记,获取、释放、检测

StripedMap是C++函数,我在网上找了一个源码,不知道是不是真的

template<typename T>
class StripedMap {

    enum { CacheLineSize = 64 };

#if TARGET_OS_EMBEDDED
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

 public:
    T& operator[] (const void *p) { 
        return array[indexForPointer(p)].value; 
    }
    const T& operator[] (const void *p) const { 
        return const_cast<StripedMap<T>>(this)[p]; 
    }

#if DEBUG
    StripedMap() {
        // Verify alignment expectations.
        uintptr_t base = (uintptr_t)&array[0].value;
        uintptr_t delta = (uintptr_t)&array[1].value - base;
        assert(delta % CacheLineSize == 0);
        assert(base % CacheLineSize == 0);
    }
#endif
};

StripedMap代码中值得注意的是,indexForPointer函数,表示了哈希表的计算方式,先对指针对象地址进行4和9的移位,在取两个移位结果的异或值,结果除以当前队列中排队线程数来得到一个哈希值。CacheLineSize表示整个哈希表的最大的大小。意味着@synchronized是有上限有性能瓶颈的。

接下来我们来解读一下下半部分的代码

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


void _destroySyncCache(struct SyncCache *cache)
{//释放缓存数组
    if (cache) free(cache);
}


static SyncData* id2data(id object, enum usage why)
{
    //根据object获取 自旋锁对象和SyncData链表(全局哈希)
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

    //对于同一个线程来说,有两种缓存方式:
    //第一种:快速缓存(fastCache),适用于一个线程一次只对一个对象加锁的情况,用宏SUPPORT_DIRECT_THREAD_KEYS来标识
    //这种情况意味着同一时间内,线程缓存中只有一个SyncCacheItem对象
    //键值SYNC_DATA_DIRECT_KEY和SYNC_COUNT_DIRECT_KEY分别对应SyncCacheItem结构体中的SyncData对象和lockCount.
#if SUPPORT_DIRECT_THREAD_KEYS
    // Check per-thread single-entry fast cache for matching object
    //用于标识当前线程的是否已使用fastCache
    bool fastCacheOccupied = NO;
    //调用tls_get_direct函数获取SyncData对象
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
         //标识fastCache已被使用
        fastCacheOccupied = YES;
         //比较fastCache中的SyncData对象中的object与当前同步对象object是否为同一个对象
        if (data->object == object) {
            // Found a match in fast cache.
              //fastCache中的对象恰好是当前同步对象object,则后续处理直接使用fastCache中SyncData对象
            uintptr_t lockCount;

            result = data;
            //获取当前线程对应当前SyncData对象已经加锁的次数
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            //创建SyncData对象的时候,count设置为1。如果count为0,则应该被释放。因此 <0 表示无效的SyncData对象
            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);
                //已加锁次数为0,表示当前线程对当前同步对象object达到锁平衡,因此不需要再持有当前同步对象。
                if (lockCount == 0) {
                    // remove from fast cache
                    //将对应的SyncData对象从线程缓存中移除
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    //此函数为原子操作函数,用于对32位的threadCount整形变量执行减一操作,且确保线程安全。
                    //因为可能存在同一时间多个线程对一个threadCount进行加减操作,避免出现多线程竞争。
                    //不同于lockCount,threadCount是多个线程共享的一个变量,用于记录对一个对象加锁的线程个数,
                    //threadCount对应的SyncData对象除了线程缓存中持有之外,还存在于全局哈希表sDataLists中,
                    //sDataLists哈希表是多个线程共享的数据结构,因此存在多线程访问的可能。
                    //而lockCount则与线程一一对应且存储在线程的缓存区中,不存在多线性读写问题,因此不需要加锁。
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    //这是第二章缓存方式:使用SyncCache结构体来维护一个SyncCacheItem数组,这样一个线程就可以处理对多个同步对象。值得注意的是SyncCache与线程也是一对一的关系。
    //获取当前线程缓存区中的SyncCache对象
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        //遍历SyncCache对象中的SyncCacheItem数组,匹配当前同步对象object
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (item->data->object != object) continue;

            // Found a match.
            //当前同步对象object已存在的SyncCache中
            //获取对应的SyncData对象
            result = item->data;
            //无效的SyncData对象
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
            //后续操作同fastCache一样,参考fastCache的注释
            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
                    //此函数为原子操作函数,确保线程安全,用于对32位的threadCount整形变量执行加一操作
                    //表示占用当前同步对象的线程数加1。
                    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.
    
    //如果当前线程中的缓存中没有找到当前同步对象对应的SyncData对象,则在全局哈希表中查找
    //因为全局哈希表是多个线程共享的数据结构,因此需要进行加锁处理
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        //遍历当前同步对象obejct在全局哈希表中的SyncData链表。
        //这里之所以使用链表,是因为哈希表的hash算法不能确保hash的唯一性,存在多个对象对应一个hash值的情况。
        for (p = *listp; p != NULL; p = p->nextData) {
              //哈希表中存在对应的SyncData对象
            if ( p->object == object ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                //此函数为原子操作函数,确保线程安全,用于对32位的threadCount整形变量执行加一操作
                //表示占用当前同步对象的线程数加1。
                OSAtomicIncrement32Barrier(&result->threadCount);
                goto done;
            }
            //用于标记一个空闲的SyncData对象
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        //由于此时同步对象object没有对应的SyncData对象,因此RELEASE与CHECK都属于无效操作
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it
        //如果没有找到匹配的SyncData对象且存在空闲的SyncData对象,则直接使用,不需要创建新的SyncData,以提高效率。
        if ( firstUnused != NULL ) {
            result = firstUnused;
            //关联当前同步对象
            result->object = (objc_object *)object;
            //重置占用线程为1
            result->threadCount = 1;
            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.
    
    //到这一步说明需要新建一个SyncData对象
    result = (SyncData*)calloc(sizeof(SyncData), 1);
    result->object = (objc_object *)object;
    result->threadCount = 1;
    //创建递归互斥锁
    new (&result->mutex) recursive_mutex_t();
    //以“入栈”的方式加入当前同步对象object对应的SyncData链表
    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.
        //只有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");

         //fastCache缓存模式
#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) {
            // Save in fast thread cache
            //直接缓存新建的SyncData对象
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            //设置加锁次数为1
            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++;
        }
    }

    return result;
}


BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);


// 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);//根据obj获取SyncData对象
        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); //根据obj获得SyncData对象
        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;
}

从上面源码可以看出。
objct_sync_enter 会根据obj对象的HASH值在SyncList链表中查询其的SyncData,先根据缓存策略在当前线程的缓存链表中查找是否有相同对象。如果有则直接操作缓存对象并返回对象,如果没有则先加锁再在全局哈希表中查找是否有相同对象,如果有则直接解锁操作后返回对象,如果没有就会创建SyncData对象并返回。拿到SyncData对象后进行加锁。
object_sync_exit 跟 object_sync_enter基本相同,只是操作有所不同。最后拿到SyncData对象后会进行解锁操作。

为什么要强转appDelegateClassName?
从编译器代码和源码里可以看出,所有的操作都依赖于传入的obj指针的内存地址,假如obj指针,在执行过程中,被置nil或者释放了。则在最后执行object_sync_exit的时候无法正常对原本的obj对象内存地址进行解锁。那么一旦后面系统分配了同一个内存地址给其他对象,而这个对象也需要进行@synchronized加锁,则会直接死锁。因为上一个没有被解锁。因此使用强转来弱化对原obj对象的使用。如果原对象被置nil或释放,也不会影响转换后的指针。object_sync_enter与object_sync_exit传参就是一致的,不会出现死锁。

为什么定义这两个结构体:_SYNC_EXIT与_FIN
从object_sync_enter与object_sync_exit的作用可以知道,代码执行完毕后必须要解锁,而解锁的时机很重要。而因为C++中没有try{}catch{}finally{}语句,所以不能在finally{}调用objc_sync_exit函数。因此定义了_SYNC_EXIT结构体,通过在_SYNC_EXIT结构体的析构体执行时机来判断什么时候需要执行objc_sync_exit函数。同理,通过_FIN结构体的析构体来寻找合适的时机来判断是否需要抛出异常。

至此@synchronized底层原理就解析完毕。

补充一下锁的执行效率。


image.png

上面的图是网上找的资料,由于加锁我基本上都使用的GCD,且平时工作里业务不复杂性能要求也不是非常高,所以很少遇到性能问题。其他锁也没有实际测过,因此直接在网上找的资料,跟自己的理解比较接近。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335