iOS底层 - @synchronized(下)

2022-01-11 16:09:57 浏览数 (1)

线程缓存查找流程

代码语言:javascript复制
//2.从线程缓存中查找如果fetch_cache(NO)参数是NO表示去只是去查找
SyncCache *cache = fetch_cache(NO);
if (cache) {
    unsigned int i;
    //遍历缓存中的 SyncCacheItem
    for (i = 0; i < cache->used; i  ) {
        SyncCacheItem *item = &cache->list[i];
        if (item->data->object != object) continue;
        // Found a match.
        result = item->data;//如果线程缓存中查找到item将item->data赋值给result
        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
                // 将当前的item中的数据设置为空的数据,然后cache->used被使用的个数进行减减
                cache->list[i] = cache->list[--cache->used];
                // atomic because may collide with concurrent ACQUIRE
                // 线程threadCount个数进行减一
                OSAtomicDecrement32Barrier(&result->threadCount);
            }
            break;
        case CHECK:
            // do nothing
            break;
        }
        //如果线程缓存中查询到就直接返回result
        return result;
    }
}

(滑动显示更多)

线程缓存的查找流程和tls查找流程基本相似,的就是数据结构的类型不同。在线程缓存中通过遍历循环查找需要加锁的对象,详细的流程上面注释的很清楚。

  • fetch_cache探究
代码语言:javascript复制
static SyncCache *fetch_cache(bool create)
{
    _objc_pthread_data *data;

    //查询或者创建线程数据
    data = _objc_fetch_pthread_data(create);
    if (!data) return NULL;//如果data = nil 返回NULL

    if (!data->syncCache) {
        if (!create) {
            return NULL;
        } else {
            //首次创建 4 个 SyncCacheItem
            int count = 4;
            //开辟内存空间
            data->syncCache = (SyncCache *)
                calloc(1, sizeof(SyncCache)   count*sizeof(SyncCacheItem));
            //初始化的个数等于 4 
            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;
}

(滑动显示更多)

  • 根据create去查找线程数据_objc_pthread_data,如果data不存在直接返回NULL
  • 如果data->syncCache不存在且create== YES去为SyncCache和SyncCacheItem开辟内存空间第一次开辟4个SyncCacheItem,初始化的个数allocated = 4。每个SyncCacheItem里面的默认数据都是0
  • 如果初始化的个数和使用的个数相进行2倍扩容。返回SyncCache

_objc_fetch_pthread_data探究

代码语言:javascript复制
 _objc_pthread_data *_objc_fetch_pthread_data(bool create)
{
    _objc_pthread_data *data;
    //先去tls查找线程数据,如果data存在直接返回
    //如果data不存在且create = NO 返回nil
    data = (_objc_pthread_data *)tls_get(_objc_pthread_key);
    //如果data不存在且create = YES 则去开辟内存创建_objc_pthread_data
    if (!data  &&  create) {
        data = (_objc_pthread_data *)
            calloc(1, sizeof(_objc_pthread_data));
        tls_set(_objc_pthread_key, data);
    }

    return data;
}

(滑动显示更多)

_objc_fetch_pthread_data流程:

  • 在tls中查找_objc_pthread_data,如果查询到直接返回data
  • 如果data不存在且create = NO,返回nil
  • 如果data不存在且create = YES,则去开辟内存创建_objc_pthread_data
  • 所以在create = NO只会去tls中查找data,create = YES先去查找data,如果没有就去开 辟内存创建data

多线程加锁同一对象

为什么说下面这段代码是多线程加锁同意对象呢,因为如果是单个线程内存操作会直接走上面的tls查找流程和线程缓存查找流程:

代码语言:javascript复制
lockp->lock();
//多线程操作流程
{
    SyncData* p;
    SyncData* firstUnused = NULL;
    //哈希表中的SyncList的data如果有值即*listp有值
    //遍历循环单向链表中的SyncData
    for (p = *listp; p != NULL; p = p->nextData) {
        //查询到需要加锁的对象
        if ( p->object == object ) {
            result = p;//赋值操作
            // atomic because may collide with concurrent RELEASE
            //线程数threadCount进行加1操作,threadCount多个线程对该对象进行加锁操作
            OSAtomicIncrement32Barrier(&result->threadCount);
            //跳转到done流程。此时的result还是存在各个线程的tls中或者线程缓存中
            goto done;
        }
        if ( (firstUnused == NULL) && (p->threadCount == 0) )
            firstUnused = p;
    }
    ...
}

(滑动显示更多)

多线程操作流程

  • 首先会根据object在哈希表中找到下标对应的SyncList,然后判断SyncList的data有值
  • 如果SyncList的data有值中有值,则在单向链表找到相应的加锁对象,进行threadCount 操作,然后跳转done流程,在done流程里存储在对应线程的tls中或者线程缓存中

创建SyncData

代码语言:javascript复制
posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));
//赋值操作
result->object = (objc_object *)object;
result->threadCount = 1;//默认 threadCount = 1
new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);//创建一个递归锁
result->nextData = *listp; //哈希下标如果一样头插法形成链表的形式
*listp = result;//更新哈希表中的值

(滑动显示更多)

第一次给对象加锁时,会创建一个SyncData,简单的说就是一个SyncData绑定一个对象,一个对象有且只有一个SyncData。

如果不同对象哈希的下标是一样的这样就会形成单向链表,而插入的方式采用的是头插法。

done流程

代码语言:javascript复制
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
        // fastCacheOccupied = NO 表示当前线程的tls里面没有数据
        // 此时把第一次需要加锁创建的SyncData和lockCout = 1 存储在tls
        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
        {   //当在同一线程对另一个对象进行加锁注意(上一个加锁的对象没有进行解锁)
            //此时的SyncData和lockCount存储在线程缓存中
            // 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;

(滑动显示更多)

done流程

  • 当前线程第一个进行加锁操作的对象,此时会把SyncData和lockCout=1 存储在tls,而且当前线程的tls不会改变除非解锁。一个线程的tls只存储第一次加锁的SyncData和lockCout
  • 当在同一线程对另一个对象进行加锁注意(上一个加锁的对象没有进行解锁),此时的SyncData和lockCount存储在线程缓存中

至此整个id2data方法的源码已经探究完,下面通过案例分析下。

案例分析

单线程单对象递归加锁

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LWPerson * p = [LWPerson alloc];
        @synchronized (p) {
            NSLog(@"p第一次进来");
            @synchronized (p) {
                NSLog(@"p第二次进来");
                @synchronized (p ) {
                    NSLog(@"p第三次进来");
                };
            };
        };

    return 0;
}

(滑动显示更多)

在第一个@synchronized打下断点进行调试

第一次对对象加锁*listp、data以及cache都是空,说哈希表中和tls以及线程缓存中都没有查找到。那么就会进入创建SyncData流程,为了方便测试我把在非真机状态下的 StripeCount = 2。

图中显示此时的哈希表中的数据都是空的没有数据,断点往下走一步,继续调试。

*listp = result更新哈希表中data的值,此时result的nextData等于*listp,*listp前面打印的值是空的,所以result的nextData也是空的,*listp = result以后此时*listp已经被赋值。继续调试走到done流程:

图中断点显示此时第一次创建的SyncData和lockCount存储在tls中

在第二个@synchronized打下断点进行调试:

对同一个对象再次加锁此时*listp和data都有值,那么此时会走到tls查找data的流程。

lockCount = 2说明又加锁一次,然后更新tls中lockCount的值,然后直接返回result。

在第三个@synchronized打下断点进行调试:

很明显在单线程中对同一个对象进行递归加锁,SyncData和lockCount存储在tls,而没有缓存的事。

单线程多对象递归加锁

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    LWPerson * p  = [LWPerson alloc];
    LWPerson * p1 = [LWPerson alloc];
    LWPerson * p2 = [LWPerson alloc];

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (p) {
            NSLog(@"p第一次进来");
            @synchronized (p1) {
                NSLog(@"p1第二次进来");
                @synchronized (p2) {
                    NSLog(@"p2第三次进来");
                };
            };
        };
    });
    do {

    } while (1);
  }
    return 0;
}

(滑动显示更多)

在第一个@synchronized打下断点进行调试。

对p对象进行加锁,此时创建的SyncData和lockCount存储在tls中,这个上面已经探究过了。

在第二个@synchronized打下断点进行调试:

*listp有值说明p和p1的哈希下标是一样的,此时cache中也没有数据,tls中的是有data不过和p1不是同一个对象,所以此时会重新创建SyncData,然后用头插法的方式形成单向链表。

最新创建的SyncData放在拉链的最前面,此时p1的SyncData的nextData存储着p的SyncData地址:

因为当前线程的tls中是有data的而且不会改变,所以fastCacheOccupied = YES,在走done流程就会存储在线程缓存中,如果后面还有其它对象需要加锁,也都会储存在缓存中。解锁的时候缓存的数据会被清空。

多线程递归加锁

代码语言:javascript复制
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LWPerson * p  = [LWPerson alloc];
        LWPerson * p1 = [LWPerson alloc];
        dispatch_queue_t  queue = dispatch_queue_create("lw", DISPATCH_QUEUE_CONCURRENT);
        for (int i = 0; i<10;i  ) {
            dispatch_async(queue, ^{
                @synchronized (p) {
                    NSLog(@"p第一次进来");
                    @synchronized (p1) {
                        NSLog(@"p第一次进来");
                    };
                };
            });
        }
        do {

        } while (1);

    }
    return 0;
}

(滑动显示更多)

在 @synchronized (p1)打下断点进行调试:

哈希表中只有一个SyncList有值,而且SyncData里面的值threadCount = 10,说明走了多线程的流程。

而nextData的值为空,说明此时只有一个对象进行了多线程加锁,如果多线程多对象实际就是每个对象走单个对象的加锁流程。

单向链表图 后面补上。

@synchronized总结

objc_sync_exit流程和objc_sync_enter流程走的是一样的只不过一个是加锁一个是解锁

@synchronized底层是链表查找、缓存查找以及递归,是非常耗内存以及性能的

@synchronized底层封装了是一把递归锁,可以自动进行加锁解锁,这也是大家喜欢使用它的原因

@synchronized中lockCount控制递归,而threadCount控制多线程

@synchronized加锁的对象尽量不要为nil,不然起不到加锁的效果

总结

锁的世界才开始,今天探究了@synchronized递归锁。下面会探究其它类型的锁,让我们一起在锁的世界遨游。

文章由作者:嘿嘿小开发 逻辑iOS学员提供

0 人点赞