美文网首页
iOS 底层 - 多线程安全隐患之加锁

iOS 底层 - 多线程安全隐患之加锁

作者: 水中的蓝天 | 来源:发表于2020-04-08 19:58 被阅读0次

    本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢 !

    多线程安全隐患表现在那些方面 ?

    资源共享

    • 一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    • 比如:多个线程访问同一个对象同一个变量同一个文件

    当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题;

    多线程数据错乱: 顾名思义就是数据出现混乱,比如:3条线程在给变量age赋值同时又有9条线程来取这个age的值,那么取出来的值就可能会各不相同;这样就相当于是数据出现了错乱 !

    多线程数据安全:多线程访问导致了数据出现错乱,从而就可能引发数据的安全问题。比如:存取钱时多条线程同时操作可能就会造成钱越取越多或越存越少。

    图解

    多线程安全隐患分析@2x.png

    解决办法:

    • 采用线程同步技术(同步:协同步调,按预定的先后次序进行)
    • 常见的线程同步技术是:加锁
    加锁@2x.png

    性能从高到低排序
    os_unfair_lock 自旋锁
    OSSpinLock 自旋锁
    dispatch_semaphore 信号量
    pthread_mutex 互斥锁
    dispatch_queue(DISPATCH_QUEUE_SERIAL) 串行队列
    NSLock 普通(互斥)锁
    NSCondition 条件锁
    pthread_mutex(recursive) 递归锁
    NSRecursiveLock 递归锁
    NSConditionLock 条件锁
    @synchronized 递归锁

    推荐使用dispatch_semaphore (ios 4开始)pthread_mutex
    os_unfair_lock 从 IOS10开始,所以如果老版本不建议使用

    OSSpinLock 自旋锁

    • 等待锁的线程会处于忙等(busy-wait)状态,一直占用CPU资源
    • 属于High-level lock (高级锁),特点就是等不到锁就一直在循环等待
    • 目前已经不在安全,可能会出现优先级反转问题;所以从IOS10开始不推荐使用 !,建议使用os_unfair_lock
      • 如果等待锁的线程优先级较高,它会一直占用这CPU资源,优先级低的线程就无法释放锁 ;

    比如:
    开启了thread1(优先级最高)thread2(优先级最低)这两条线程来执行相同任务,如果thread2先进来执行,就会先加锁准备执行任务;
    这时候thread1刚好进来了,发现线程已经被加过锁了那它只能忙等;忙等相当于是在while循环等待,这也是需要消耗CPU给分配的资源的,由于thread1优先级最高肯定会分配到更多的资源,这样可能会造成thread2没有资源可被利用无法继续执行自己的代码,没发继续执行也就没办法解锁了,thread2家的这把锁就无法释放了!

    资源抢夺结果:thread2的无法释放,thread1一直在忙等;最终就造成死锁咯 !

    解决这种情况需要把忙等该为休眠,就是等待的这个线程让他休眠;而这种技术在os_unfair_lock中实现了!

    使用介绍:

    • 需要导入头文件#import <libkern/OSAtomic.h>
    WX20200408-213529@2x.png

    注意: OSSpinLock初始化赋值需要静态初始化,应该直接赋值;如果是直接一个方法调用,用返回值赋值给他是会报错。

    os_unfair_lock 互斥锁的一种

    • os_unfair_lock用于取代不安全的OSSpinLock,从IOS10开始才支持
    • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
    • 属于low-level lock(低级锁) ,特点等不到锁就休眠

    使用介绍:

    • 需要导入头文件#imorpt <os/lock.h>
    WX20200408-213908@2x.png

    dispatch_semaphore 信号量

    • 信号量的初始值,可以用来控制线程的并发访问的最大数量;NSOperationQueue的maxConcurrentOperationCount也可以做到
    • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
    • 等待时进行休眠
    WX20200408-214405@2x.png

    示例代码

      self.semaphore = dispatch_semaphore_create(5);
    
    - (void)otherTest
    {
    //开启20条子线程都来执行test方法
        for (int i = 0; i < 20; i++) {
            [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
        }
    }
    
    // 线程10、7、6、9、8
    - (void)test
    {
        // 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
        // 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
        //#define DISPATCH_TIME_NOW (0ull)       不需要等
        //#define DISPATCH_TIME_FOREVER (~0ull)  一直等
        dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
        
        sleep(2);
        NSLog(@"test - %@", [NSThread currentThread]);
        
        // 让信号量的值+1
        dispatch_semaphore_signal(self.semaphore);
        
        //减一再加一刚好保持不变;
        
    }
    
    

    如果每个线程都需要不同的锁可以用宏定义的方式

    #define SemaphoreBegin \
    static dispatch_semaphore_t semaphore; \
    static dispatch_once_t onceToken; \
    dispatch_once(&onceToken, ^{ \
        semaphore = dispatch_semaphore_create(1); \
    }); \
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    #define SemaphoreEnd \
    dispatch_semaphore_signal(semaphore);
    
    

    pthread_mutex

    • pthread开头的都是跨平台在Linux、unix、Windows、IOS都可以使用
    • mutex 叫做“互斥锁”,等待锁的线程会处于休眠状态
    • 此类锁不用是需要手动销毁的
    pthread_mutex@2x.png

    使用介绍:

    • 锁的属性值
     #define  PTHREAD_MUTEX_NORMAL  0             普通互斥锁
     #define  PTHREAD_MUTEX_DEFAULT  0             普通互斥锁
     #define  PTHREAD_MUTEX_ERRORCHECK  1    检查错误互斥锁
     #define  PTHREAD_MUTEX_RECURSIVE       2    递归锁(递归互斥锁)
    
    • 需要导入头文件#import <pthread.h>

    普通锁 PTHREAD_MUTEX_DEFAULT

    - (void)__initMutex:(pthread_mutex_t *)mutex
    {
        // 静态初始化
        //pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
        
        // 动态初始化锁
        pthread_mutex_init(mutex, NULL); //PTHREAD_MUTEX_DEFAULT
    
    }
    
    - (void)__saveMoney
    {
       //加锁
        pthread_mutex_lock(&_moneyMutex);
        [super __saveMoney];
        //解锁
        pthread_mutex_unlock(&_moneyMutex);
    }
    
    - (void)dealloc
    {
       //销毁锁
        pthread_mutex_destroy(&_moneyMutex);
    }
    
    

    递归锁 pthread_mutex(recursive) PTHREAD_MUTEX_RECURSIVE

    递归锁@2x.png

    注意:递归锁允许同一个线程对一把锁进行重复加锁

    条件锁

    条件锁@2x.png
    • 示例代码
    - (instancetype)init
    {
        if (self = [super init]) {
            // 初始化属性
            pthread_mutexattr_t attr;
            pthread_mutexattr_init(&attr);
            pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
            // 初始化锁
            pthread_mutex_init(&_mutex, &attr);
            // 销毁属性
            pthread_mutexattr_destroy(&attr);
            
            // 初始化条件
            pthread_cond_init(&_cond, NULL);
            
            self.data = [NSMutableArray array];
        }
        return self;
    }
    
    - (void)otherTest
    {
        [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
        
        [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
    }
    
    // 生产者-消费者模式
    生产者负责生产商品
    消费者负责购买
    生产者生产出来商品就需要通知消费者可以购买商品了,在此之前消费者可以在休眠等待
    // 线程1 消费者
    // 删除数组中的元素
    - (void)__remove
    {
        pthread_mutex_lock(&_mutex);
        NSLog(@"__remove - begin");
        
        if (self.data.count == 0) {
            // 等待就开始休眠,同时放开锁;_cond该参数是用于将来唤醒的
            pthread_cond_wait(&_cond, &_mutex);
        }
        
        [self.data removeLastObject];
        NSLog(@"删除了元素");
        
        pthread_mutex_unlock(&_mutex);
    }
    
    // 线程2 生产者
    // 往数组中添加元素
    - (void)__add
    {
        pthread_mutex_lock(&_mutex);
        
        sleep(1);
        
        [self.data addObject:@"Test"];
        NSLog(@"添加了元素");
        
        // 信号
        pthread_cond_signal(&_cond);
        // 广播
    //    pthread_cond_broadcast(&_cond);
        
        pthread_mutex_unlock(&_mutex);
    }
    
    - (void)dealloc
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
    
    

    dispatch_queue(DISPATCH_QUEUE_SERIAL)

    • 直接使用GCD的串行队列,也是可以实现线程同步的
    串行队列.png

    NSLock、 NSRecursoveLock

    • NSLock是对mutex普通锁的一个OC版本封装,说白了就是普通互斥锁!
    • NSRecursiveLock也是对mutex recursove(递归锁)的OC版本封装,API跟NSLock基本一致

      NSLock、 NSRecursoveLock@2x.png

    常用方法解读

    /**
    尝试加锁,如果加上锁就返回YES,不会阻塞线程;
    */
    - (BOOL)tryLock;
    
    /**
    传入一个时间参数limit,在这个时间之前我能够等到这把锁放开的话,我就加锁成功返回YES;
    在此之前等不到我就阻塞线程、睡觉;如果时间到了还是没有等到锁被放开,那就加锁失败返回NO!
    有返回结果后,代码就会往下执行
    */
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    
    /**
    加锁
    */
    - (void)lock;
    
    /**
    解锁
    */
    - (void)unlock;
    
    

    NSCondition

    • NSCondition是对mutexcond的OC版本封装,说白了就是条件锁
    NSCondition.png

    常用方法解读

    //等待
    - (void)wait;
    //在某个时间点之前等待,时间过了自己就会结束休眠
    - (BOOL)waitUntilDate:(NSDate *)limit;
    //发部信号
    - (void)signal;
    //发广播
    - (void)broadcast;
    
    • 示例代码
    - (instancetype)init
    {
        if (self = [super init]) {
            self.condition = [[NSCondition alloc] init];
            self.data = [NSMutableArray array];
        }
        return self;
    }
    
    - (void)otherTest
    {
        [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
        [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
    }
    
    // 生产者-消费者模式
    
    // 线程1
    // 删除数组中的元素
    - (void)__remove
    {
        [self.condition lock];
        NSLog(@"__remove - begin");
        
        if (self.data.count == 0) {
            // 等待
            [self.condition wait];
        }
        
        [self.data removeLastObject];
        NSLog(@"删除了元素");
        
        [self.condition unlock];
    }
    
    // 线程2
    // 往数组中添加元素
    - (void)__add
    {
        [self.condition lock];
        
        sleep(1);
        
        [self.data addObject:@"Test"];
        NSLog(@"添加了元素");
    
        // 信号
        [self.condition signal];
        // 广播
    //    [self.condition broadcast];
    
        [self.condition unlock];
        
    }
    

    NSConditionLock

    • NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值;本质上还是条件锁
    NSConditionLock.png

    可以实现线程间的依赖,类似NSOperationQueue的依赖;

    - (instancetype)init
    {
        if (self = [super init]) {
            self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
        }
        return self;
    }
    
    - (void)otherTest
    {
        [[[NSThread alloc] initWithTarget:self selector:@selector(__one) object:nil] start];
        
        [[[NSThread alloc] initWithTarget:self selector:@selector(__two) object:nil] start];
        
        [[[NSThread alloc] initWithTarget:self selector:@selector(__three) object:nil] start];
    }
    
    //线程__three依赖__two,__two依赖__one
    - (void)__one
    {
        [self.conditionLock lock];
        
        NSLog(@"__one");
        sleep(1);
        
        [self.conditionLock unlockWithCondition:2];
    }
    
    - (void)__two
    {
        [self.conditionLock lockWhenCondition:2];
        
        NSLog(@"__two");
        sleep(1);
        
        [self.conditionLock unlockWithCondition:3];
    }
    
    - (void)__three
    {
        [self.conditionLock lockWhenCondition:3];
        
        NSLog(@"__three");
        
        [self.conditionLock unlock];
    }
    

    @synchronized

    • @synchronized是对mutex递归锁(pthread_mutex(recursive))的封装
    • 源码查看:objc4中的objc-sync.mm文件
    • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
    • 性能差,官方不推荐使用;且编写时没有提示
    • 使用起来非常简单,是加锁技术中最简单
    @synchronized.png
    • 拿到需要加锁的对象,传入对象从散列表中取出对应的锁;
    散列表(哈希)
    static StripeMap<SyncList> sDataLists
    SyncData *data = sDataLists[obj].data
    data->mutex.lock()
    

    自旋锁、互斥锁比较

    • 临界区:加锁和解锁中间的代码区域

    什么情况使用自旋锁比较划算?

    • 预计线程等待锁的时间很短
    • 加锁的代码(临界区)经常被调用,但竞争情况很少发生(很少出现多条线程同时访问的情况)
    • CPU资源不紧张
    • 多核处理器

    什么情况使用互斥锁比较划算?

    • 预计线程等待锁的时间较长
    • 单核处理器
    • 临界区有IO操作(文件的读写操作)
    • 临界区代码复杂或者循环量大
    • 临界区竞争非常激烈

    相关文章

      网友评论

          本文标题:iOS 底层 - 多线程安全隐患之加锁

          本文链接:https://www.haomeiwen.com/subject/wdvtmhtx.html