iOS-OC底层-@synchronized分析

前言

,在我们的iOS开发中还是经常用到的,特别是在一些多线程的安全访问方面提供了提供了便捷的方案。,分为自旋锁,互斥锁,读写锁等类型。在iOS下,我们常见的包括:@synchronized,NSLock,dispatch_semphore,NSCondition,NSConditionLock,NSRecursiveLock等。本篇文章我们就重点分析下@synchronized的本质。

开始

首先从一个大家熟悉的卖票案例说起:

- (void)demo11
{
    _totalCount = 20;
    [self startSaleTicket];
}
- (void)startSaleTicket{
    dispatch_async(dispatch_queue_create("queue_1", DISPATCH_QUEUE_CONCURRENT), ^{
        for (int i = 0; i < 30; i++) {
            [self saleTicket];
        }
    });
    dispatch_async(dispatch_queue_create("queue_2", DISPATCH_QUEUE_CONCURRENT), ^{
        for (int i = 0; i < 15; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket{
    if (_totalCount > 0) {
        _totalCount--;
        sleep(0.1);
        NSLog(@"当前余票还剩:%ld张",_totalCount);
    }else{
        NSLog(@"当前车票已售罄");
    }
}

以上代码表示有两个并发队列同时执行异步的买票任务,这就意味着saleTicket在某个时刻或者多个时刻同时被多个任务访问,而导致数据的不同步,从而导致数据的不安全。输出结果:

2020-11-09 16:29:05.728246+0800 TestApp[13533:7920231] 当前余票还剩:18张
2020-11-09 16:29:05.728246+0800 TestApp[13533:7920234] 当前余票还剩:19张
2020-11-09 16:29:05.728464+0800 TestApp[13533:7920234] 当前余票还剩:17张
2020-11-09 16:29:05.728464+0800 TestApp[13533:7920231] 当前余票还剩:17张
2020-11-09 16:29:05.728604+0800 TestApp[13533:7920234] 当前余票还剩:16张
2020-11-09 16:29:05.728624+0800 TestApp[13533:7920231] 当前余票还剩:15张
2020-11-09 16:29:05.728750+0800 TestApp[13533:7920234] 当前余票还剩:13张
2020-11-09 16:29:05.728752+0800 TestApp[13533:7920231] 当前余票还剩:13张
2020-11-09 16:29:05.728925+0800 TestApp[13533:7920234] 当前余票还剩:12张
2020-11-09 16:29:05.729969+0800 TestApp[13533:7920231] 当前余票还剩:11张
2020-11-09 16:29:05.730211+0800 TestApp[13533:7920234] 当前余票还剩:10张
...............省略一些打印数据...............
2020-11-09 16:29:05.733704+0800 TestApp[13533:7920231] 当前余票还剩:1张
2020-11-09 16:29:05.734041+0800 TestApp[13533:7920234] 当前余票还剩:0张
2020-11-09 16:29:05.735104+0800 TestApp[13533:7920231] 当前车票已售罄
2020-11-09 16:29:05.735527+0800 TestApp[13533:7920234] 当前车票已售罄
2020-11-09 16:29:05.735923+0800 TestApp[13533:7920231] 当前车票已售罄

从输出的结果可以看到,整个数据一开始就发生了错误。接下来我们使用@synchronizedsaleTicket加锁然后再执行:

- (void)saleTicket{
    @synchronized (self) {
        if (_totalCount > 0) {
            _totalCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%ld张",_totalCount);
        }else{
            NSLog(@"当前车票已售罄");
        }
    }
}
2020-11-09 16:31:46.802605+0800 TestApp[13600:7921875] 当前余票还剩:19张
2020-11-09 16:31:46.802823+0800 TestApp[13600:7921875] 当前余票还剩:18张
2020-11-09 16:31:46.803009+0800 TestApp[13600:7921875] 当前余票还剩:17张
2020-11-09 16:31:46.803180+0800 TestApp[13600:7921874] 当前余票还剩:16张
2020-11-09 16:31:46.803327+0800 TestApp[13600:7921874] 当前余票还剩:15张
2020-11-09 16:31:46.803493+0800 TestApp[13600:7921875] 当前余票还剩:14张
................省略一些打印数据.................
2020-11-09 16:31:46.809983+0800 TestApp[13600:7921874] 当前余票还剩:1张
2020-11-09 16:31:46.810512+0800 TestApp[13600:7921874] 当前余票还剩:0张
2020-11-09 16:31:46.811509+0800 TestApp[13600:7921874] 当前车票已售罄
2020-11-09 16:31:46.812906+0800 TestApp[13600:7921874] 当前车票已售罄
2020-11-09 16:31:46.813443+0800 TestApp[13600:7921874] 当前车票已售罄

此时的打印结果就不会出现异常的数据了。那@synchronized是怎么做到的呐?内部都做了些什么?
我们首先通过断点并打开Always Show Disassembly来看下汇编的信息,是否有一些线索:

截屏2020-11-09 下午4.41.21.png

截屏2020-11-09 下午4.42.45.png

从汇编中,能看到的是在执行前后多了objc_sync_enterobjc_sync_exit指令,可能有用先记录下。
接下来我们通过clang来看下@synchronized最终转化为了什么:
首先,我们在main.m中,添加几行代码:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    @synchronized (appDelegateClassName) {
        NSLog(@"@synchronized");
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

通过clang编译后的代码:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    { id _rethrow = 0; id _sync_obj = (id)appDelegateClassName; 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);

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_yy_htpy_x9s09v1zf7ms0jgytwr0000gn_T_main_54c387_mi_0);
    } 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 UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

比较凌乱,我们整理一下:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    /* @autoreleasepool */
    { __AtAutoreleasePool __autoreleasepool;
        appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
    }
    /* @synchronized */
    {
        id _rethrow = 0;
        id _sync_obj = (id)appDelegateClassName;
        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);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_yy_htpy_x9s09v1zf7ms0jgytwr0000gn_T_main_54c387_mi_0);
            
        } 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 UIApplicationMain(argc, argv, __null, appDelegateClassName);
}

这里第一个作用域的内容是关于@autoreleasepool相关的,关于这部分内容可以直接看我的iOS-OC底层-@autoreleasepool分析 这篇文章。第二个作用域中的内容就是关系@synchronized了,还是挺复杂的。这里我们并不需要看太多:
1.首先需要注意到的是objc_sync_enter(_sync_obj);这跟在汇编看到的是一致的
2.然后是一个try{}包裹逻辑,内部有一个struct _SYNC_EXIT结构体,并且包换一个构造方法_SYNC_EXIT(id arg)和析构方法~_SYNC_EXIT(),
3._sync_exit(_sync_obj);初始化结构体执行_SYNC_EXIT(id arg)
4.NSLog(),这是需要加锁的代码块主体。
5.当try{}执行完时,会触发结构体的~_SYNC_EXIT()的析构方法,即objc_sync_exit(sync_exit)方法。
所以这里简单的理解就是:
objc_sync_enter(_sync_obj) ->代码主体 -> objc_sync_exit(sync_exit)
catch部分我们就不做分析了,当发生异常后会执行objc_exception_throw
到此,我们需要探索的重点就锁定在了objc_sync_enter()objc_sync_exit()的两个方法的具体实现中来,通过汇编跟踪下这两个方法在哪个库中(以objc开头,通常是在libobjc.dylib中):

截屏2020-11-09 下午5.17.58.png

果不其然,确实就在libobjc中,查看源码:

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

这里的参数id obj其实就是@synchronized时传入的参数;判断obj是否存在,如果存在,就通过id2data()方法读取一个SyncData* data,然后通过data->mutex.lock()来上锁。这里要说一下mutex:
mutex:用于保证在任何时刻,都只能有一个线程访问该对象。 当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
这也就意味着,@synchronized实质上是一种(递归)互斥锁。
而当obj == nil时,即@synchronized(nil),看else内部注释:@synchronized(nil) does nothing
这里还是要看下objc_sync_nil()做了什么:

BREAKPOINT_FUNCTION(
    void objc_sync_nil(void)
);
#   define BREAKPOINT_FUNCTION(prototype)                             \
    OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
    prototype { asm(""); }

这里是一个宏定义的实现,但内部也没有做什么实质性的处理。

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

再看下int objc_sync_exit(id obj),当obj存在,同样会通过id2data(obj, RELEASE)去获取SyncData* data,如果存在,会通过data->mutex.tryUnlock();去解锁。当obj不存在,则dose nothing
从上面的源码中我们可以发现,两个方法内部都调用了同样的方法id2data(),
只是第二个传参有所不同,一个是ACQUIRE,一个是RELEASE。我们重点看下id2data()方法:
先预览下这部分代码:

截屏2020-11-09 下午5.49.18.png

我们把代码大致分成如图的4个部分:

  1. #if SUPPORT_DIRECT_THREAD_KEYS .. #endif部分
  2. SyncCache *cache = fetch_cache(NO);部分
  3. lockp->lock();lockp->unlock(); 部分
  4. if(result)部分

这里我们要注意代码注释还是非常有价值的,首先是判断是否支持DIRECT_THREAD_KEYS的方式读取缓存,如果不支持就通过per-thread cache读取缓存;如果缓存中不存在则尝试通过in-use list查找,找到后插入缓存;如果都没有,即第一次进入,则通过posix_memalign()创建新的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;

#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部分:
这里先扩展一下TLS相关的概念:线程局部存储(Thread Local Storage,TLS),是操作系统为线程单独提供的私有空间,通常只有有限的容量。通常通过pthread库中的:
pthread_key_create(),
pthread_getspecific(),
pthread_setspecific(),
pthread_key_delete()
等API来存取一些数据或保存一些状态。简单的理解就是给线程也提供了一些KVC的存取操作的能力。
这里主要存储就是两个值SYNC_DATA_DIRECT_KEY:SyncData *data
SYNC_COUNT_DIRECT_KEY:uintptr_t lockCount
这里先看下SyncData的数据结构:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

SyncData内部实际是是一个单链表结构,每一个结构都会指向nextData;
threadCount来记录当前线程数,object即传入的参数,mutex核心的锁结构。
接下来,如果找到了data,将data赋值给result,同时将lockCount ++或者lockCount--,并通过tls_set_direct()更新到对应的key。当lockCount == 0时,会将线程缓存值SYNC_DATA_DIRECT_KEY置为NULL,同时threadCount --;
第2部分:
这里其实还是读取缓存,还是先看下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;

只是这里查询的结构有所不同,其最终还是去查询SyncDatalockCount。然后其他的增减逻辑就一模一样了。
第三部分:

//Thread cache didn't find anything.
//Walk in-use list looking for matching object

第四部分:

// Allocate a new SyncData and add to list.
首次执行,新创建 SyncData,
// Save in fast thread cache
// Save in thread cache

最后通过一个图标来总结下:

图1

图2 内部关联逻辑

从图中可以看出:

  • 锁的内部首先是一个SyncCache结构体,包含着一个list数组,存放着SyncCacheItem类型的数据,该元素表示某一个线程被锁定的任务;
  • SyncCacheItem结构包含一个SyncData *data,和int lockCount,其中lockCount记录这这条线程上锁的总数,data记录着被锁定的任务;
  • SyncData,内部包含了一个nextData,也是SyncData类型,表示下一个被锁定的任务,所以这里是一个单链表的结构,来记录整个任务链。
使用中的注意事项

@synchronized,由于需要各种链表的查询,创建等操作,所以在整个iOS锁的性能排名上还是比较靠后的。但由于它使用起来非常的简单,而且不需要做额外的上锁解锁操作,所以在项目中使用频率还是挺高的。
先写一个案例:

- (void)demo33
{
    for (int i = 0; i < 100000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self->_testArray = [NSMutableArray array];
        });
    }
}

在执行demo33时,会发生EXC_ACCESS崩溃

截屏2020-11-10 下午4.32.49.png

这是由于多个异步线程在给self.testArray赋值,即retain新值,release旧值,但在某一个瞬间,retainrelease在多个异步线程的情况下,发生资源抢占,导致没有成对执行,而导致了release野指针的情况,如下:

2020-11-10 16:37:08.198551+0800 TestApp[45349:8517346] @synchronized
2020-11-10 16:37:08.364108+0800 TestApp[45349:8517491] *** -[__NSArrayM release]: message sent to deallocated instance 0x6000032b0ae0
2020-11-10 16:37:08.364115+0800 TestApp[45349:8517494] *** -[__NSArrayM release]: message sent to deallocated instance 0x60000324cea0

接下来,我们给代码添加一个@synchronized

- (void)demo33
{
    for (int i = 0; i < 100000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self.testArray) {
                self.testArray = [NSMutableArray array];
            }
        });
    }
}

此时还是发生了崩溃,

截屏2020-11-10 下午4.40.16.png

同样是由于objc_release去释放了一个野指针导致。
这里要注意的就是@synchronized()的参数了,self.testArray在多次的赋值过程中,在某个瞬间可能为nil,而@synchronized(nil)意味着加锁失败,就相当于没有锁,所以这里崩溃的情况就跟之前的一样了。
再次修改代码,把@synchronized()传入的参数修改为self:

- (void)demo33
{
    for (int i = 0; i < 100000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self) {
                self.testArray = [NSMutableArray array];
            }
        });
    }
}

此时执行代码,结果显示正常。
所以这里要注意,当使用@synchronized锁时,要保证该锁传入的参数在使用期间不能为nil,否则锁将失去意义,比如这里使用生命周期更长的self,但这里不仅限于self,只要保证在使用期间不为nil就是可以的。再者就是基于@synchronized的性能表现不是很好,所以还是尽量选择其他锁来替代它。

总结

这篇文章有问题,写的不好!!!!摸不着头脑!!!!谨慎阅读!!!!

你可能感兴趣的:(iOS-OC底层-@synchronized分析)