ios 底层分析-锁

396 阅读8分钟

前言

锁对我们而言不陌生但是又很陌生,当多个线程操作同一个资源的时候为了内存安全,需要对资源进行保护,那么我们需要使用锁。常用的锁如@synchronizedNSRecursiveLockNSLock、以及属性的原子操作锁atomic等等,他们有什么区别呢?我们该如何理解并正确使用呢?

疑问?

看一下下面的例子

   - (void)viewDidLoad {
        [super viewDidLoad];
         //假设有100张电影票
        self.ticketCount = 100;
       //线程1
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 60; i++) {
                [self saleTicket];
            }
        });
       //线程2
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            for (int i = 0; i < 50; i++) {
                [self saleTicket];
            }
        });
    }
    - (void)saleTicket{
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
        }else{
            NSLog(@"当前车票已售罄");
        }
}

输出: image.png 分析:如上图所示,如果不加锁,输出当前余票就会发生错乱,我们想看到的应该是顺序递减的,那么在上面saleTicket方法中加一个@synchronized锁就可以解决这种多线程导致的数据不安全的问题。

- (void)saleTicket{
    @synchronized (self) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%lu张",(unsigned long)self.ticketCount);
        }else{
            NSLog(@"当前车票已售罄");
        }
    }
}

那么再看下面的例子

//初始化NSLock锁
self.mylock=[[NSLock alloc]init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
        //block处理业务代码
        [self.mylock lock];
        if (value > 0) {
            NSLog(@"current value = %d",value);
            testMethod(value - 1);
        }
        [self.mylock unlock];
    };
    testMethod(10);
    });

输出:

**current value = 10**

分析:testMethod是一个处理业务的代码块,里面进行了递归调用,如果使用NSLock锁就会产生死锁,因为理论上输出日志应该是从10开始递减,那么换成@synchronized如何呢?

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
        @synchronized (self) {
            if (value > 0) {
                NSLog(@"current value = %d",value);
               testMethod(value - 1);
            }
        }

    };
    testMethod(10);
});

输出:

current value = 10
current value = 9
current value = 8
current value = 7
current value = 6
current value = 5
current value = 4
current value = 3
current value = 2
current value = 1

分析:看到这我们至少知道@synchronized可以解决NSLock因为递归导致的死锁问题,说明NSLock是一把非递归锁,如果把NSLock换成递归锁NSRecursiveLock也能解决这个问题,那么NSRecursiveLock能解决多线程的问题吗?

self.recursiveLock = [[NSRecursiveLock alloc] init];
for (int i= 0; i<10; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [self.recursiveLock lock];
                if (value > 0) {
                    NSLog(@"current value = %d",value);
                    testMethod(value - 1);
                }
            [self.recursiveLock unlock];
        };
        testMethod(10);
    });
    }

image.png 结果:在多线程的情况下NSRecursiveLock递归锁会导致程序奔溃,而换成@synchronized同样可以解决在多线程下的递归问题。看样子@synchronized确实有点屌,不仅能多线程加锁而且可以递归调用,下面就分析下源码看看它是如何实现的。

@synchronized

随便写一个@synchronized通过汇编看看底层调用了什么符号,然后再跟进源码看细节,也可以通过clang编译一下源文件看看@synchronized编译成了什么。 image.png 通过汇编发现@synchronized底层是调用了objc_sync_enter加锁和objc_sync_exit解锁,我们进objc源码看下这两个函数。

objc_sync_enter、objc_sync_exit

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;

}

分析:objc_sync_enter、objc_sync_exit逻辑上是一样的,一个是加锁一个是解锁。首先判断obj是否为空,如果不为空就构造一个SyncData对象并且加锁,如果为空就什么也不操作,所以在使用@synchronized(obj)时必须要传递一个对象否则加不了锁。加锁操作是通过对象SyncData获取的mutex.lock(),看下SyncData结构以及id2data方法是如何生成的SyncData。

SyncData

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;//单向链表结构 指向下一个syncdata
    DisguisedPtr<objc_object> object;//把传入进来的obj构造成一个统一的结构
    int32_t threadCount;  //操作锁的线程数
    recursive_mutex_t mutex;//递归锁

} SyncData;

分析:通过SyncData对象我们稍微有了一点点明悟,threadCount应该就是@synchronized可以多线程加锁的原因,recursive_mutex_t应该就是@synchronized可以递归的原因,至于nextData我们目前只知道是一个链表结构,接下来看下创建这个对象的函数id2data()

static SyncData* id2data(id object, enum usage why)
{   
    //从哈希表SyncList中获取object对象的锁,目的是保证该方法代码块的内存安全
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    //从哈希表SyncList中通过object对象的哈希下标获取syncData地址
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;
#if SUPPORT_DIRECT_THREAD_KEYS
    bool fastCacheOccupied = NO;
    //1.从线程局部存储中查找根据object对应的SyncData,tls为线程局部存储,每个线程都有唯一一个
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    if (data) {
       //同一个对象单线程递归加锁,加锁就lockcount++,解锁就lockCount--,
       if (data->object == object) {
       //....
       }
     }
    //2.从线程缓存中查找SyncData,如果是加锁lockcount++,解锁就lockCount-
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
     //...
    }
    lockp->lock();
    //3.多线程进入的流程,进行threadcount++
    {
      //省略....  
    }

    //4.创建SyncData,线程默认为1,nextData指定为上一个SyncData,头插法
    //第一次加锁或者单线程不同对象加锁会进入
    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给哈希表赋值
    *listp = result;
 done:
    lockp->unlock();
    if (result) {
        //同一线程,对象第一次加锁,tls中没有,设置tls
        if (!fastCacheOccupied) {
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);
        } else 
        {//同一线程,其他对象保存在线程缓存,tls内存有限
            // 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;
}

分析:创建SyncData流程如下

  • 哈希表SyncList中通过object对象的哈希下标获取listp,注意是通过对象的哈希下标寻址,在存储的时候如果对象的哈希下标一样,就通过拉链法存储。
  • tls(线程局部存储)中获取object对应的syncData,如果获取到了,就把该对象在tls中lockcount++或者lockcount--,加还是减根据是加锁还是解锁判断。每个对象都绑定一个syncData,每个线程的tls都不一样,如果是同一个对象单线程递归加锁,就会进入该流程。
  • 线程缓存中获取object对应的syncData,如果获取到了,就把该对象在线程缓存中lockcount++或者lockcount--,加还是减根据是加锁还是解锁判断。tls内存容量很小,一般同一个线程第一个对象会存在tls中,其他对象都会存在线程缓存中。
  • tls和线程缓存都找不到该对象,说明是不同线程或不同对象又或者是第一次加锁,如果listp不为空,就循环遍历listp的拉链表,直到找到与object对应的syncData,使它threadcount++,简单点说就是多线程对同一个对象加锁时会进入该流程。
  • tls和线程缓存都找不到该对象并且非多线程,根据object创建syncData,并且指定syncData中属性nextData为上一个syncData,这是头插法,也就是每次新来的数据会插在之前数据的前边
  • 同一线程第一次加锁会把syncData插入到tls,同一线程第二次加锁会把syncData插入到线程缓存。

哈希表SyncList源码如下:

static StripedMap<SyncList> sDataLists;
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif
// ......
}
struct SyncList {
    SyncData *data;
    spinlock_t lock;
    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

static sDataLists是一个全局的哈希表,真机情况的存储SyncList个数是8个,其它环境64个,SyncList是一个封装着SyncData的结构体,使用拉链法来存储 SyncData,同一个线程不同对象加锁时很大概率会触发拉链存储,拉链存储的前提是对象的哈希值一样,用下面这张图辅助理解下这个哈希表的结构 image.png

lldb断点调试演示拉链

为了方便断点调试演示拉链,把源码中StripedMap里StripeCount改为2增加hash冲突的概率。

同一线程不同对象加锁

LGTeacher* lgter1=[[LGTeacher alloc]init];
LGTeacher* lgter2=[[LGTeacher alloc]init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    @synchronized (lgter1) {
           NSLog(@"对象1加锁");
        @synchronized (lgter2) {
           NSLog(@"对象2加锁");
        }
    }
});

断点打在第一个@synchronized image.png 对象lgter1加锁,此时*listp值为空,创建syncData对象result并且把result的nextData指向*listp即为空对象。过了241行后,*listp被赋值成了对象result,此时哈希表被更新有值了,继续往下走会把对象lgter1对应的syncData存进tls

断点打在第二个@synchronized image.png 对象lgter2加锁时,此时*listp不会空,说明lgter2和lgter1哈希下标是一样的listp是根据下标在哈希表中取值的,继续往下走不会进tls,因为虽然是同一个线程但不是同一个对象,也不会进线程缓存中查找,最后会创建一个新的syncData对象result并且把result的nextData指向listp即为上一个对象lgter1的syncData,这样就形成了拉链image.png 继续往下走对象会被存进线程缓存而不是tls,tls只有线程第一次给对象加锁时才会被存入。

总结

  • @synchronized 可递归可多线程
  • @synchronized 是通过全局哈希表,拉链法存储syncData
  • @synchronized 线程有两种存储方式,tls和线程缓存
  • @synchronized 拉链触发的前提是对象的哈希下标一样
  • @synchronized(self),常用self对象加锁是为了保证锁的生命周期,以及方便存储和释放