ITPub博客

首页 > 应用开发 > IT综合 > 进行婚恋相亲系统开发,需要了解的底层结构

进行婚恋相亲系统开发,需要了解的底层结构

IT综合 作者:云豹科技晓彤 时间:2021-10-28 16:50:48 0 删除 编辑

前言

锁对我们而言不陌生但是又很陌生,当多个线程操作同一个资源的时候为了内存安全,婚恋相亲系统开发需要对资源进行保护,那么我们需要使用锁。常用的锁如 @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只有线程第一次给对象加锁时才会被存入。
声明:本文由云豹科技转发自顶风尿一丈博客,如有侵权请联系作者删除


来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/70002045/viewspace-2839542/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
音视频软件开发相关知识科普账号

注册时间:2021-06-11

  • 博文量
    163
  • 访问量
    36465