@synchronized原理和block的类型

0a000581fca74098bc53b876f5e957ae.jpeg 持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

今天我们继续探索@synchronized原理和block的类型及用法。

一. @synchronized原理

1、@synchronized

在main函数中添加:

@autoreleasepool {
        NSObject *obj = [NSObject alloc];
        @synchronized (obj) {

        }
    }
复制代码

然后clang main.m文件生成main.cpp.找到main函数: image.png objc底层最终执行的是objc_sync_enter(_sync_obj); objc_sync_exit(sync_exit);两个函数。 我们进入objc源码看看:

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


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 的结构体。 image.png data->mutex.lock();递归加锁。threadCount 记录有几个线程使用到了@synchronized的递归锁。 我们进入id2data 看看源码:

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;
    //..........................第一段省略..............................//
    }

#endif

    // TLS 存储只属于当前线程的数据
    // Check per-thread cache of already-owned locks for matching object
    //检查每个线程的 cache 去匹配一个对象objcet
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
//..........................第二段省略..............................//

    }

\


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

//..........................第三段省略..............................//

    }



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

        }

//..........................第四段省略..............................//

    }

    return result;

}
复制代码

&LOCK_FOR_OBJ 和 &LIST_FOR_OBJ的对应实现。 image.png image.png 所以:@synchronized真机上的性能高于模拟器上的性能。

StripedMap 是缓存带spinlock锁能力的类或者结构体

StripedMap

StripedMap 的主要作⽤是⽤来缓存 带spinlock锁能⼒的类或者结构体 。 它的⼯作原理是什么呢?⽐如说这个SyncList,我们想⼀下,如果说系统在全局只初始化⼀张 SyncList⽤来管理所有对象的加锁和解锁操作,其实也是可以。只是,效率会很慢,因为每个对象 在操作这个表的时候,都需要等待其他对象操作完解锁之后才能进⾏。或者说,系统为每⼀个对象 都创建⼀个SyncList,其实也是可以的,只是内存的消耗会⾮常⼤。

所以,苹果就⽤这个 StripedMap 来解决这个问题,提前准备⼀定个数的 SyncList 放这⾥,然后 在调⽤的时候均匀的进⾏分配。

单向链表

单向链表(Linkedlist)是⼀种常⻅的基础数据结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,链表由⼀连串节点组成每个节点保存数据和到下⼀个节点的地址。单向链表只能向⼀个⽅向遍历。

TLS(Thread Local Storage)

TLS就是线程局部存储,是操作系统为线程单独提供的私有空间,能存储只属于当前线程的⼀些数据

image.png ACQUIRE代表加锁操作,RELEASE代表解锁操作。 image.png

@synchronized的底层原理

核⼼图: image.png 整体思路就是:@synchronize针对某个对象,也就是我们给@synchronize传的参数,每⼀条线程都有⼀把递归锁,⽽且记录了每条线程加锁的次数,这样就能通过这俩点,对每条线程⽤不同的递归锁来进⾏加锁和解锁的的操作,从⽽达到多线程递归调⽤的⽬的。

二. block的类型

block有三种类型:堆block,栈block,全局block。

我们依次来了解它们:

1.首先全局block(NSGlobalBlock):

    void (^block)(void) = ^{

    };
    block();
    NSLog(@"%@",block);
    **2022-06-16 20:33:30.614794+0800 Block的类型[1520:24575] <__NSGlobalBlock__: 0x10324a050>**
复制代码

block如果没有使⽤外部变量,或者只使⽤静态变量和全局变量,那⼀定是全局blcok。

2.堆block(NSMallocBlock):

    int a = 1;
    void (^block)(void) = ^{
        NSLog(@"%d",a);
    };
    block();
    NSLog(@"%@",block);
    **2022-06-16 21:08:53.004907+0800 Block的类型[1819:34534] <__NSMallocBlock__: 0x6000011c12f0>**
复制代码

block如果使⽤了外部变量,⽽且不是静态变量或全局变量,如果赋值给强引⽤的是堆block。 2.栈block(NSStackBlock):

    int a = 1;
    void (^__weak block)(void) = ^{
        NSLog(@"%d",a);
    };
    block();
    NSLog(@"%@",block);
    **2022-06-16 21:12:11.795813+0800 Block的类型[1895:37345] <__NSStackBlock__: 0x7ff7bbb78078>**
复制代码

对于这三种类型的block遵循俩个原则:

  • block如果没有使⽤外部变量,或者只使⽤静态变量和全局变量,那⼀定是全局blcok。
  • block如果使⽤了外部变量,⽽且不是静态变量或全局变量,如果赋值给强引⽤的是堆block,如果赋值给弱引⽤的是栈blcok。

在看一个例子:

- (void)blockDemo1{

    NSObject *objc = [NSObject new];

    NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));

    void(^block1)(void) = ^{

        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));

    };

    block1();

    void(^__weak block2)(void) = ^{

        NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));

    };

    block2();
}
复制代码

分析代码:objc这个时候的引用计数是1,block1是堆block里objc计数+2=3,block2是栈block里的objc计数+1=4。 打印结果: 1,3,4 。 image.png 堆block为什么是+2呢? 用clang命令生成对应的main.cpp然后找到如下代码:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__weak weakSelf;//外部变量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
      // 你写在block代码块里的代码
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

看到__main_block_impl_0是一个结构体,block的本质就是一个结构体。看到代码impl.isa = &_NSConcreteStackBlock; block在创建的时候是一个栈block,哪block是在什么时候变成堆block呢? image.png 运行如上代码,查看汇编: image.png 然后下一个符号断点objc_retainBlockimage.png 发现objc_retainBlock->_Block_copy调用了_Block_copy。

进入block源码: image.png image.png 直接上菜了。看到代码:
1.如果block已经在堆上计数+1,返回block。
2.如果block在全局flags & BLOCK_IS_GLOBAL直接返回block本身。
3.如果block在栈上,需要拷贝block到堆上(重新开辟一个内存地址)result上,最后result->isa = _NSConcreteMallocBlock;指向了_NSConcreteMallocBlock 堆类型。

为什么block要⽤copy关键字修饰?

因为block在创建的时候,它的内存是分配在栈上的,⽽不是在堆上。栈区的特点是:对象随时有 可能被销毁,⼀旦被销毁,在调⽤的时候,就会造成系统的崩溃。所以我们要使⽤copy把它拷⻉到堆上。在ARC下, 对于block使⽤copy与strong其实都⼀样, 因为block的retain就是⽤copy来实现的, 所以在ARC下 block使⽤ copy 和 strong 都可以。

总结:

@synchronized原理:会对参数对象的每条线程,都加一把递归锁,如果能在快速缓存中找到syncCahcheItem并且当前data与obj相同对item->lockcount ++或--操作。如果没有在快速缓存中找到,先判断快速缓存表有没数据,没有数据直接保存data也就是当前@synchronized的参数到快速缓存中。快速缓存表中有数据,则把表中的数据copy到线程缓存中,从⽽达到多线程递归调⽤的⽬的。

堆block,栈block,全局block对于这三种类型的block遵循俩个原则:

  • block如果没有使⽤外部变量,或者只使⽤静态变量和全局变量,那⼀定是全局blcok。
  • block如果使⽤了外部变量,⽽且不是静态变量或全局变量,如果赋值给强引⽤的是堆block,如果赋值给弱引⽤的是栈blcok。

主要探讨了:堆block为什么会从栈block变成堆block呢?在创建堆block的时候,会把当前栈上的block拷贝到堆其中主要的函数是objc_retainBlock->_Block_copy

猜你喜欢

转载自juejin.im/post/7109865290103521294
今日推荐