美文网首页iOS精选
iOS-底层原理(23)-多线程的安全隐患+11种同步解决方案

iOS-底层原理(23)-多线程的安全隐患+11种同步解决方案

作者: 路飞_Luck | 来源:发表于2018-09-16 22:45 被阅读40次
    一 多线程的安全隐患
    • 资源共享
      • 1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
      • 比如多个线程访问同一个对象、同一个变量、同一个文件

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

    代码例子如下

    /** 卖1张票 */
    - (void)saleTicket {
        int oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        
        NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
    }
    
    /** 卖票演示 */
    - (void)ticketTest {
        self.ticketsCount = 15;
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        // 窗口一
        dispatch_async(queue, ^{
            for (int i = 0; i < 5; i++) {
                [self saleTicket];
            }
        });
        // 窗口二
        dispatch_async(queue, ^{
            for (int i = 0; i < 5; i++) {
                [self saleTicket];
            }
        });
        // 窗口三
        dispatch_async(queue, ^{
            for (int i = 0; i < 5; i++) {
                [self saleTicket];
            }
        });
    }
    

    执行结果

    image.png
    多线程安全隐患分析
    image.png
    二 多线程安全隐患的解决方案
    • 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
    • 常见的线程同步技术是:加锁
    image.png
    三 iOS中的线程同步方案
    • OSSpinLock
    • os_unfair_lock
    • pthread_mutex
    • dispatch_semaphore
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSRecursiveLock
    • NSCondition
    • NSConditionLock
    • @synchronized
    各种同步方案实现如下
    3.1 OSSpinLock
    • OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
    • 目前已经不再安全,可能会出现优先级反转问题
    • 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
    • 需要导入头文件#import <libkern/OSAtomic.h>
    • 重要方法
      • OSSpinLock lock = OS_SPINLOCK_INIT; 初始化锁
      • bool result = OSSpinLockTry(&_lock); 尝试加锁(如果需要等待,就不尝试加锁,直接返回false,如果不需要等待就加锁,返回true)
      • OSSpinLockLock(&_lock);// 加锁
      • OSSpinLockUnlock(&_lock); //解锁

    代码例子如下

    #import <libkern/OSAtomic.h>
    @property (assign, nonatomic) OSSpinLock lock;
    
    // 初始化锁
    self.lock = OS_SPINLOCK_INIT;
    
    /** 卖1张票 */
    - (void)saleTicket {
        // 加锁
        OSSpinLockLock(&_lock);
        
        int oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        
        NSLog(@"还剩%d张票 - %@", oldTicketsCount, [NSThread currentThread]);
        
        // 解锁
        OSSpinLockUnlock(&_lock);
    }
    

    执行结果

    image.png
    3.2 os_unfair_lock
    • os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持
    • 从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
    • 需要导入头文件#import <os/lock.h>
    • 重要方法
      • os_unfair_lock moneyLock = OS_UNFAIR_LOCK_INIT; //初始化
      • os_unfair_lock_trylock(&_ticketLock); // 尝试加锁
      • os_unfair_lock_lock(&_ticketLock); // 加锁
      • os_unfair_lock_unlock(&_ticketLock); // 解锁

    3.3 pthread_mutex

    • mutex叫做”互斥锁”,等待锁的线程会处于休眠状态
    • 需要导入头文件#import <pthread.h>
    • 重要方法
    // 初始化属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
    // 初始化锁
    pthread_mutex_init(mutex, &attr);
    // 尝试加锁
    pthread_mutex_trylock(&_ticketMutex);
    // 加锁
    pthread_mutex_lock(&_ticketMutex);
    // 解锁
    pthread_mutex_unlock(&_ticketMutex);
    // 销毁属性
    pthread_mutexattr_destroy(&attr);
    
    3.4 pthread_mutex递归锁实现
    - (void)__initMutex:(pthread_mutex_t *)mutex {
        // 递归锁:允许同一个线程对一把锁进行重复加锁
        
        // 初始化属性
        pthread_mutexattr_t attr;
        pthread_mutexattr_init(&attr);
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        // 初始化锁
        pthread_mutex_init(mutex, &attr);
        // 销毁属性
        pthread_mutexattr_destroy(&attr);
    }
    

    调用

    - (void)otherTest {
        pthread_mutex_lock(&_mutex);
        NSLog(@"%s", __func__);
        
        static int count = 0;
        if (count < 10) {
            count++;
            [self otherTest];
        }
        
        pthread_mutex_unlock(&_mutex);
    }
    

    打印结果

    image.png
    3.5 pthread_mutex – 条件
    image.png
    3.6 NSLock
    • NSLock是对mutex普通锁的封装

    重要方法如下

    • NSLock *lock = [[NSLock alloc] init];初始化
    • [lock lock] 加锁
    • [lock unlock] 解锁
    3.7 NSRecursiveLock
    • NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
    3.8 NSCondition
    • NSCondition是对mutexcond的封装
      重要方法
    @interface NSCondition: NSObject <NSLocking>
    - (void)wait;   // 等待
    - (BOOL)waitUntilDate:(NSDate *)limit;  // 只等待到什么时候
    - (void)signal; // 发信号
    - (void)broadcast;  // 发广播
    @end
    

    代码例子如下

    - (void)otherTest {
        // remove和add方法不确定谁先执行
        [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
        
        [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
    }
    
    // 删除数组中的元素
    - (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(@"添加了元素");
        
        // 信号
        NSLog(@"发出信号");
        [self.condition signal];
        // 广播
    //    [self.condition broadcast];
        
        sleep(2);
        
        [self.condition unlock];
    }
    

    运行结果

    image.png

    更改执行顺序

    // 往数组中添加元素
    - (void)__add {
        [self.condition lock];
        
        sleep(1);
        
        [self.data addObject:@"Test"];
        NSLog(@"添加了元素");
        
        [self.condition unlock];
        
        sleep(2);
        
        // 信号
        NSLog(@"发出信号");
        [self.condition signal];
        // 广播
    //    [self.condition broadcast];
    }
    

    运行结果

    image.png

    wait不仅仅需要接受到信号后才能执行,而且必须具备加锁条件,这个时候才会接着往下执行。

    3.9 NSConditionLock
    • NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
    image.png

    代码例子如下

    @property (strong, nonatomic) NSConditionLock *conditionLock;
    
    - (instancetype)init {
        if (self = [super init]) {
    //        [[NSConditionLock alloc] init]; // 默认为0
            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];
    }
    
    - (void)__one {
        [self.conditionLock lockWhenCondition:1];
        
        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];
    }
    
    

    执行结果

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

    代码例子如下

    @property (strong, nonatomic) dispatch_queue_t ticketQueue;
    
    self.ticketQueue = dispatch_queue_create("ticketQueue", DISPATCH_QUEUE_SERIAL);
    
    - (void)__saleTicket {
        dispatch_sync(self.ticketQueue, ^{
            [super __saleTicket];
        });
    }
    
    3.10 dispatch_semaphore
    • nsemaphore叫做信号量
    • 信号量的初始值,可以用来控制线程并发访问的最大数量
    • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
    image.png

    代码例子如下

    @property (strong, nonatomic) dispatch_semaphore_t ticketSemaphore;
    @property (strong, nonatomic) dispatch_semaphore_t moneySemaphore;
    
    self.ticketSemaphore = dispatch_semaphore_create(1);
    self.moneySemaphore = dispatch_semaphore_create(1);
    
    - (void)__drawMoney {
        dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
        
        [super __drawMoney];
        
        dispatch_semaphore_signal(self.moneySemaphore);
    }
    
    - (void)__saveMoney {
        dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER);
        
        [super __saveMoney];
        
        dispatch_semaphore_signal(self.moneySemaphore);
    }
    
    - (void)__saleTicket {
        dispatch_semaphore_wait(self.ticketSemaphore, DISPATCH_TIME_FOREVER);
        
        [super __saleTicket];
        
        dispatch_semaphore_signal(self.ticketSemaphore);
    }
    
    3.11 @synchronized
    • @synchronized是对mutex递归锁的封装
    • 源码查看:objc4中的objc-sync.mm文件
    • @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
    @synchronized(obj) {
        任务
    }
    

    obj 可以是同一个实例对象,类对象,静态变量

    代码例子如下

    - (void)__drawMoney {
        @synchronized([self class]) {
            [super __drawMoney];
        }
    }
    
    - (void)__saveMoney {
        @synchronized([self class]) { // objc_sync_enter
            [super __saveMoney];
        } // objc_sync_exit
    }
    
    - (void)__saleTicket {
        static NSObject *lock;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            lock = [[NSObject alloc] init];
        });
        
        @synchronized(lock) {
            [super __saleTicket];
        }
    }
    
    // 递归锁 - 可以递归
    - (void)otherTest {
        @synchronized([self class]) {
            NSLog(@"123");
            [self otherTest];
        }
    }
    
    四 iOS线程同步方案性能比较

    性能从高到低排序

    • os_unfair_lock
    • OSSpinLock
    • dispatch_semaphore
    • pthread_mutex
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSCondition
    • pthread_mutex(recursive)
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized
    五自旋锁、互斥锁比较

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

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

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

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

    本文参考MJ底层原理教程,非常感谢MJ老师


    项目连接地址 - 多线程安全+解决方案

    相关文章

      网友评论

        本文标题:iOS-底层原理(23)-多线程的安全隐患+11种同步解决方案

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