美文网首页iOS进阶程序员
iOS底层原理探索—多线程的“锁”

iOS底层原理探索—多线程的“锁”

作者: 劳模007_Mars | 来源:发表于2019-08-13 21:27 被阅读1次

    探索底层原理,积累从点滴做起。大家好,我是Mars。

    往期回顾

    iOS底层原理探索 — OC对象的本质
    iOS底层原理探索 — class的本质
    iOS底层原理探索 — KVO的本质
    iOS底层原理探索 — KVC的本质
    iOS底层原理探索 — Category的本质(一)
    iOS底层原理探索 — Category的本质(二)
    iOS底层原理探索 — 关联对象的本质
    iOS底层原理探索 — block的本质(一)
    iOS底层原理探索 — block的本质(二)
    iOS底层原理探索 — Runtime之isa的本质
    iOS底层原理探索 — Runtime之class的本质
    iOS底层原理探索 — Runtime之消息机制
    iOS底层原理探索 — RunLoop的本质
    iOS底层原理探索 — RunLoop的应用
    iOS底层原理探索 — 多线程的本质
    iOS底层原理探索 — 多线程的经典面试题

    前言

    多线程是iOS开发中很重要的一个环节,无论是开发过程还是在面试环节中,多线程出现的频率都非常高。我们会通过几篇文章的探索,深入浅出的分析多线程技术。

    我们通过之前两篇文章的分析,大致对多线程技术有了一定的了解,认识到了多线程技术的强大。但是多线程在应用过程中存在一定的安全隐患,今天我们继续分析,来看多线程中如何解决这些问题。

    多线程的安全隐患

    当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题。
    比如多个线程访问同一个对象、同一个变量、同一个文件。
    具体场景为:存钱取钱、购买车票等。
    我们模拟一下卖票的场景。一共有15张票,开设三个窗口卖票,每个窗口卖掉5张,我们来看一下多个线程访问的时候会出现什么问题:

    /** 卖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];
            }
        });
    }
    

    我们打印一下执行结果:


    模拟卖票.png

    我们发现,经过15次卖票后,正常来说15张票已经全部卖完才对,但是最后一次打印还剩5张票。这就出现了线程安全问题。

    我们再来看下面一个例子:

    多线程安全隐患.png
    例如上图中,一个integer类型的对象值为17,当线程A和线程B同时访问到的时候,线程A做+1操作,同时线程B也做+1操作。

    由于两个线程访问到integer类型对象时的值都为17,分别做+1操作后变量的值变为18。但是实际的结果应为做了两次+1操作,值应该为19。这就出现了问题。

    那么多线程同样提供了解决方案:使用线程同步技术。

    线程同步技术

    常见的线程同步技术是加锁
    我们再讲上面的例子优化:

    加锁.png
    如上图,我们应用了加锁技术后,当线程A访问和修改变量时,会加锁,进行+1操作,此时变量值变为18,然后对其解锁。解锁后,当线程B访问和修改变量时,此时变量的值已经为18,再进行+1操作,完成修改。这样就保证了线程安全。

    iOS中的线程同步方案

    iOS中为我们提供了一下几种线程同步方案:

    • OSSpinLock —— 自旋锁
    • os_unfair_lock
    • pthread_mutex —— 互斥锁
    • NSLock
    • NSRecursiveLock
    • NSCondition
    • NSConditionLock
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • dispatch_semaphore —— 信号量
    • @synchronized

    接下来我们逐一对以上方案进行分析:

    1、OSSpinLock 自旋锁

    OSSpinLock叫做自旋锁,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。目前已经不再安全,可能会出现优先级反转问题。在iOS10版本以后就不再支持这一技术。
    如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
    需要导入头文件#import <libkern/OSAtomic.h>

    //初始化锁
    OSSpinLock lock = OS_SPINLOCK_INIT; 
    //尝试加锁(如果需要等待,就不尝试加锁,直接返回false,如果不需要等待就加锁,返回true)
    bool result = OSSpinLockTry(&_lock); 
    // 加锁
    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);
    }
    
    加锁后卖票.png

    通过打印可以看到,加锁后线程安全,卖票结果也正确。

    我们在使用OSSpinLock自旋锁时,系统会报出警告,告诉我们不推荐使用OSSpinLockLock,在iOS 10.0中弃用。并推荐使用<os / lock.h>中的os_unfair_lock_lock()来代替。

    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、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);
    

    互斥锁的type有以下几种:

    #define PTHREAD_MUTEX_NORMAL        0 普通锁
    #define PTHREAD_MUTEX_ERRORCHECK    1
    #define PTHREAD_MUTEX_RECURSIVE     2  递归锁
    #define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL
    
    pthread_mutex递归锁
    // 初始化锁的属性
    pthread_mutexattr_t attr;
    pthread_attr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    // 初始化锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, &attr);
    

    pthread_mutex条件

    // 初始化锁
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    // 初始化条件
    pthread_cond_t condition;
    pthread_cond_init(&condition, NULL);
    // 等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
    pthread_cond_wait(&condition, &mutex);
    // 激活一个等待该条件的线程
    pthread_cond_signal(&condition);
    // 激活所有等待该条件的线程
    pthread_cond_broadcast(&condition);
    // 销毁相关资源
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&condition);
    

    4、NSLock

    NSLock是对mutex普通锁的封装

    // 初始化锁
    NSLock *lock = [[NSLock alloc] init];
    // 尝试加锁
    [lock tryLock];
    // 指定Date之前尝试加锁
    [lock lockBeforeDate:[NSDate date]];
    // 加锁
    [lock lock];
    // 解锁
    [lock unlock];
    

    5、NSRecursiveLock

    NSRecursiveLock是对mutex 递归锁的封装,APINSLock基本一致。

    // 初始化锁
    NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
    // 尝试加锁
    [lock tryLock];
    // 指定Date之前尝试加锁
    [lock lockBeforeDate:[NSDate date]];
    // 加锁
    [lock lock];
    // 解锁
    [lock unlock];
    

    6、NSCondition

    NSCondition是对mutex和条件的封装

    // 初始化锁
    NSCondition *condition = [[NSCondition alloc] init];
    // 加锁
    [condition lock];
    // 等待条件
    [condition wait];
    // Date之前等待条件
    [condition waitUntilDate:[NSDate date]];
    // 激活一个等待该条件的线程
    [condition signal];
    // 激活所有等待该条件的线程
    [condition broadcast];
    // 解锁
    [condition unlock];
    

    7、NSConditionLock

    NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。

    // 初始化锁
    NSConditionLock *condition = [[NSConditionLock alloc] init];
    NSConditionLock *condition = [[NSConditionLock alloc] initWithCondition:1];
    // 加锁 - 可设置条件值
    [condition lockWhenCondition:1 beforeDate:[NSDate date]];
    [condition lockBeforeDate:[NSDate date]];
    [condition lockWhenCondition:1];
    [condition lock];
    [condition tryLock];
    [condition tryLockWhenCondition:1];
    // 解锁
    [condition unlockWithCondition:1];
    [condition unlock];
    

    8、dispatch_queue(DISPATCH_QUEUE_SERIAL)串行队列

    使用GCD的串行队列,实现线程同步

    dispatch_queue_t queue = dispatch_queue_create("lock_queue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(queue, ^{
        // 需要执行的任务
    });
    

    9、dispatch_semaphore信号量

    semaphore叫做信号量,信号量的初始值,可以用来控制线程并发访问的最大数量。信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步。

    // 信号量的初始值
    int value = 1;
    // 初始化信号量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
    // 如果信号量的值 >0,就 -1,然后往下执行代码
    // 如果信号量的值 <=0,就会休眠等待,直到信号量的值变成 >0
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    // 信号量的值 +1
    dispatch_semaphore_signal(semaphore);
    

    10、@synchronized

    @synchronized是对mutex 递归锁的封装。可以查看源码:objc4中的objc-sync.mm文件。
    @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作。obj 可以是同一个实例对象,类对象,静态变量

    @synchronized(obj) {
        //需要执行的任务
    }
    

    iOS线程同步方案性能比较

    以上几种锁的性能从高到低排序:

    • os_unfair_lock
    • OSSpinLock
    • dispatch_semaphore
    • pthread_mutex
    • dispatch_queue(DISPATCH_QUEUE_SERIAL)
    • NSLock
    • NSCondition
    • pthread_mutex(recursive)
    • NSRecursiveLock
    • NSConditionLock
    • @synchronized

    自旋锁、互斥锁比较

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

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

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

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

    我们简单的介绍了一下多线程中的线程安全问题,以及解决这些问题的线程同步方案,篇幅有限,实际案例会再后续文章中为大家解读。

    更多技术知识请关注公众号
    iOS进阶


    iOS进阶.jpg

    相关文章

      网友评论

        本文标题:iOS底层原理探索—多线程的“锁”

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