美文网首页
iOS源码解析:多线程<二>线程同步

iOS源码解析:多线程<二>线程同步

作者: 雪山飞狐_91ae | 来源:发表于2018-11-29 10:56 被阅读28次

    多线程的安全隐患

    在使用多线程的过程中,一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,同一个变量,同一个对象,同一个文件。试想一下,三个线程同时向一个文件写东西,那势必会造成混乱。
    下面以取钱存钱为例:


    9577BEE5-5B64-45EB-9ACC-32B668DF1F93.png

    在这个例子中,起初余额中有1000,存钱的线程首先读出余额1000,紧接着取钱的线程又取出余额1000,然后存钱的线程又存入了1000,所以把余额修改为了2000,之后,取钱的线程取出了500,由于之前读出的余额是500,所以将余额修改为1000-500=500,这样最终的余额就变成了500。按照正常的情况,余额应该是1500,这样就出现了混乱。

    以车站卖票为例,车站中有多个窗口卖票,就相当于是多线程来处理 BC5F94D2-8C48-407D-9A75-CF38449AC543.png

    起始票数是1000,第一个卖票的站点先读取的票的余额,过了一会第二个卖票的站点也读取了票的余额,然后第一个站点卖出了一张票,因此把票数余额修改为了999,过了一会第二个站点也卖了一张票,把票数余额修改为了999,这样一来,票就永远卖不完了。
    我们用代码实现一下卖票的过程:

    @property (nonatomic, assign)int ticketsCount;
    - (void)saleTicket{
        //这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显
        int oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        
        NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);
    }
    
    - (void)saleTickets{
        
        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];
            }
        });
        
    }
    

    打印结果:

    2018-09-26 15:12:52.746209+0800 TEST[10226:312194] 最后还剩的票数13 线程<NSThread: 0x600000461740>{number = 5, name = (null)}
    2018-09-26 15:12:52.746209+0800 TEST[10226:312193] 最后还剩的票数14 线程<NSThread: 0x600000460780>{number = 4, name = (null)}
    2018-09-26 15:12:52.746245+0800 TEST[10226:312195] 最后还剩的票数14 线程<NSThread: 0x600000460500>{number = 3, name = (null)}
    2018-09-26 15:12:52.746414+0800 TEST[10226:312194] 最后还剩的票数12 线程<NSThread: 0x600000461740>{number = 5, name = (null)}
    2018-09-26 15:12:52.746552+0800 TEST[10226:312193] 最后还剩的票数11 线程<NSThread: 0x600000460780>{number = 4, name = (null)}
    2018-09-26 15:12:52.746650+0800 TEST[10226:312195] 最后还剩的票数10 线程<NSThread: 0x600000460500>{number = 3, name = (null)}
    2018-09-26 15:12:52.746707+0800 TEST[10226:312194] 最后还剩的票数9 线程<NSThread: 0x600000461740>{number = 5, name = (null)}
    2018-09-26 15:12:52.746730+0800 TEST[10226:312193] 最后还剩的票数8 线程<NSThread: 0x600000460780>{number = 4, name = (null)}
    2018-09-26 15:12:52.746913+0800 TEST[10226:312195] 最后还剩的票数7 线程<NSThread: 0x600000460500>{number = 3, name = (null)}
    2018-09-26 15:12:52.747049+0800 TEST[10226:312194] 最后还剩的票数6 线程<NSThread: 0x600000461740>{number = 5, name = (null)}
    2018-09-26 15:12:52.747301+0800 TEST[10226:312193] 最后还剩的票数5 线程<NSThread: 0x600000460780>{number = 4, name = (null)}
    2018-09-26 15:12:52.747861+0800 TEST[10226:312194] 最后还剩的票数4 线程<NSThread: 0x600000461740>{number = 5, name = (null)}
    2018-09-26 15:12:52.747861+0800 TEST[10226:312195] 最后还剩的票数4 线程<NSThread: 0x600000460500>{number = 3, name = (null)}
    2018-09-26 15:12:52.748157+0800 TEST[10226:312193] 最后还剩的票数3 线程<NSThread: 0x600000460780>{number = 4, name = (null)}
    2018-09-26 15:12:52.749157+0800 TEST[10226:312195] 最后还剩的票数2 线程<NSThread: 0x600000460500>{number = 3, name = (null)}
    

    可以看到产生了混乱,最后剩余的票数并不为0。

    然后继续用代码实现取钱存钱的过程

    @property (nonatomic, assign)int money;
    - (void)moneyTest{
        
        self.money = 100;
        
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        //存钱的线程
        dispatch_async(queue, ^{
            for (int i = 0; i < 10; i++) {
                [self saveMoney];
            }
        });
        //取钱的线程
        dispatch_async(queue, ^{
            for (int i = 0; i < 10; i++) {
                [self drawmoney];
            }
        });
    }
    
    //存钱
    - (void)saveMoney{
        
        int oldMoney = self.money;
        sleep(.2);
        oldMoney += 50;
        self.money = oldMoney;
        
        NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
    }
    //取钱
    - (void)drawmoney{
        
        int oldMoney = self.money;
        sleep(.2);
        oldMoney -= 20;
        self.money = oldMoney;
        
        NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
    }
    

    打印结果:

    2018-09-26 15:27:13.265434+0800 TEST[10568:324343] 取20 还剩80元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.265459+0800 TEST[10568:324337] 存50 还剩150元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.265587+0800 TEST[10568:324337] 存50 还剩180元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.265589+0800 TEST[10568:324343] 取20 还剩130元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.265685+0800 TEST[10568:324337] 存50 还剩230元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.265693+0800 TEST[10568:324343] 取20 还剩210元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.265771+0800 TEST[10568:324337] 存50 还剩260元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.265853+0800 TEST[10568:324343] 取20 还剩240元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.266059+0800 TEST[10568:324337] 存50 还剩290元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.266210+0800 TEST[10568:324343] 取20 还剩270元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.266343+0800 TEST[10568:324337] 存50 还剩320元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.266485+0800 TEST[10568:324343] 取20 还剩300元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.266667+0800 TEST[10568:324337] 存50 还剩350元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.266844+0800 TEST[10568:324343] 取20 还剩330元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.267284+0800 TEST[10568:324337] 存50 还剩380元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.267373+0800 TEST[10568:324343] 取20 还剩360元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.267496+0800 TEST[10568:324337] 存50 还剩410元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.267866+0800 TEST[10568:324343] 取20 还剩390元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    2018-09-26 15:27:13.268062+0800 TEST[10568:324337] 存50 还剩440元 - <NSThread: 0x60400026b180>{number = 3, name = (null)}
    2018-09-26 15:27:13.268578+0800 TEST[10568:324343] 取20 还剩420元 - <NSThread: 0x604000269c80>{number = 4, name = (null)}
    

    从最后剩余的钱数来看就完全不对,数据发生了明显的混乱。

    那么多线程的安全隐患怎么解决呢?解决方案就是使用线程同步技术,常见的线程同步技术是加锁。
    iOS中的线程同步方案有下面这些:

    OSSpinLock
    • OSSpinlock叫做"自旋锁",等待锁的线程会处于忙等状态,一直占用CPU资源
    • 目前已经不再安全,可能会出现优先级反转的问题,即如果等待锁的线程优先级较高,它会一直占用着CPU的资源,优先级低的线程就无法释放锁。
      关于OSSpinLock的API:
        //初始化
        OSSpinLock lock = OS_SPINLOCK_INIT;
        //尝试加锁看,如果需要等待就不加锁,直接返回false,如果不需要等待就加锁,返回true。
        bool result = OSSpinLockTry(&lock);
        //加锁
        OSSpinLockLock(&lock);
        //解锁
        OSSpinLockUnlock(&lock);
    

    下面我们使用OSSpinLock来解决卖票的资源争夺的问题:

    - (void)saleTicket{
        
        //加锁
        OSSpinLockLock(&_lock);
        //这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显
        int oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        
        NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);
        
        //解锁
        OSSpinLockUnlock(&_lock);  
    }
    

    我们看一下打印结果:

    2018-09-26 15:59:05.225340+0800 TEST[11218:345833] 最后还剩的票数14 线程<NSThread: 0x600000473f80>{number = 3, name = (null)}
    2018-09-26 15:59:05.225623+0800 TEST[11218:345833] 最后还剩的票数13 线程<NSThread: 0x600000473f80>{number = 3, name = (null)}
    2018-09-26 15:59:05.225799+0800 TEST[11218:345833] 最后还剩的票数12 线程<NSThread: 0x600000473f80>{number = 3, name = (null)}
    2018-09-26 15:59:05.225946+0800 TEST[11218:345833] 最后还剩的票数11 线程<NSThread: 0x600000473f80>{number = 3, name = (null)}
    2018-09-26 15:59:05.226248+0800 TEST[11218:345833] 最后还剩的票数10 线程<NSThread: 0x600000473f80>{number = 3, name = (null)}
    2018-09-26 15:59:05.227334+0800 TEST[11218:345826] 最后还剩的票数9 线程<NSThread: 0x60400027c5c0>{number = 4, name = (null)}
    2018-09-26 15:59:05.227480+0800 TEST[11218:345826] 最后还剩的票数8 线程<NSThread: 0x60400027c5c0>{number = 4, name = (null)}
    2018-09-26 15:59:05.227709+0800 TEST[11218:345826] 最后还剩的票数7 线程<NSThread: 0x60400027c5c0>{number = 4, name = (null)}
    2018-09-26 15:59:05.228151+0800 TEST[11218:345826] 最后还剩的票数6 线程<NSThread: 0x60400027c5c0>{number = 4, name = (null)}
    2018-09-26 15:59:05.233128+0800 TEST[11218:345826] 最后还剩的票数5 线程<NSThread: 0x60400027c5c0>{number = 4, name = (null)}
    2018-09-26 15:59:05.237517+0800 TEST[11218:345827] 最后还剩的票数4 线程<NSThread: 0x604000274700>{number = 5, name = (null)}
    2018-09-26 15:59:05.238065+0800 TEST[11218:345827] 最后还剩的票数3 线程<NSThread: 0x604000274700>{number = 5, name = (null)}
    2018-09-26 15:59:05.238499+0800 TEST[11218:345827] 最后还剩的票数2 线程<NSThread: 0x604000274700>{number = 5, name = (null)}
    2018-09-26 15:59:05.239221+0800 TEST[11218:345827] 最后还剩的票数1 线程<NSThread: 0x604000274700>{number = 5, name = (null)}
    2018-09-26 15:59:05.239897+0800 TEST[11218:345827] 最后还剩的票数0 线程<NSThread: 0x604000274700>{number = 5, name = (null)}
    

    可以看到现在的输出没有任何问题了。
    线程加锁的原理就是,当某一个线程首次访问资源时,对该资源加锁,当另外一个线程要访问该资源时首先判断锁有没有加上,没有的话就加锁然后访问资源,如果锁已经加上了,那么就会等待,等待锁打开。

    下面再用OSSpinLock来完成存钱取钱的加锁:

    //存钱
    - (void)saveMoney{
        
        OSSpinLockLock(&_lock);
        int oldMoney = self.money;
        sleep(.2);
        oldMoney += 50;
        self.money = oldMoney;
        
        NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
        
        OSSpinLockUnlock(&_lock);
    }
    //取钱
    - (void)drawmoney{
        
        OSSpinLockLock(&_lock);
        int oldMoney = self.money;
        sleep(.2);
        oldMoney -= 20;
        self.money = oldMoney;
        
        NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
        
        OSSpinLockUnlock(&_lock);
    }
    

    看一下打印结果:

    2018-09-26 16:45:14.317794+0800 TEST[12223:379269] 存50 还剩150元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.317953+0800 TEST[12223:379269] 存50 还剩200元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.318071+0800 TEST[12223:379269] 存50 还剩250元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.318182+0800 TEST[12223:379269] 存50 还剩300元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.318374+0800 TEST[12223:379269] 存50 还剩350元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.318500+0800 TEST[12223:379269] 存50 还剩400元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.318587+0800 TEST[12223:379269] 存50 还剩450元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.318689+0800 TEST[12223:379269] 存50 还剩500元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.318823+0800 TEST[12223:379269] 存50 还剩550元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.319047+0800 TEST[12223:379269] 存50 还剩600元 - <NSThread: 0x604000471e80>{number = 3, name = (null)}
    2018-09-26 16:45:14.320129+0800 TEST[12223:379270] 取20 还剩580元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.320242+0800 TEST[12223:379270] 取20 还剩560元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.320347+0800 TEST[12223:379270] 取20 还剩540元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.320459+0800 TEST[12223:379270] 取20 还剩520元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.320588+0800 TEST[12223:379270] 取20 还剩500元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.320693+0800 TEST[12223:379270] 取20 还剩480元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.320900+0800 TEST[12223:379270] 取20 还剩460元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.321222+0800 TEST[12223:379270] 取20 还剩440元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.321331+0800 TEST[12223:379270] 取20 还剩420元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    2018-09-26 16:45:14.321548+0800 TEST[12223:379270] 取20 还剩400元 - <NSThread: 0x60000027d080>{number = 4, name = (null)}
    
    OSSpinLock目前已经不能使用的原因

    OSSpinLock目前不建议使用的原因主要是会出现优先级反转。假设有3个线程线程1,线程2,线程3,那么如果这三个线程的优先级是一样的,那么CPU会平均的分配时间给这3个线程,比如首先给线程1 10ms去处理事件,然后给线程2 10ms去处理事件,再给线程3 10ms去处理事件,这样把时间切成碎片去处理,给人的感觉就像是三个线程一起在处理事件。但是当三个线程的优先级不一样的时候就会出现一些问题了,加入线程1的优先级较高,线程2的优先级较低,线程2首先访问资源,首先给资源加锁,这个时候线程1再去访问资源的时候,检查到锁已经加上了,所以就会在外面忙等,由于优先级很高,所以CPU分配给线程1的时间很多,分配给线程2的时间很少,这样会导致线程2没有时间来处理事件,锁很久不能打开,线程1长时间在外面等着,有点类似于死锁。

    为了更加直管的观察各种锁,现在把存钱取钱卖票的业务逻辑抽到一个基类中,名为BaseDemo,主要代码如下:

    @interface BaseDemo()
        
    @property (nonatomic, assign)int money;
    
    @property (nonatomic, assign)int ticketsCount;
    
    @end
    
    @implementation BaseDemo
    
    - (void)moneyTest{
        
        self.money = 100;
        
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        //存钱的线程
        dispatch_async(queue, ^{
            for (int i = 0; i < 10; i++) {
                [self saveMoney];
            }
        });
        //取钱的线程
        dispatch_async(queue, ^{
            for (int i = 0; i < 10; i++) {
                [self drawMoney];
            }
        });
    }
    
    //存钱
    - (void)saveMoney{
        
        int oldMoney = self.money;
        sleep(.2);
        oldMoney += 50;
        self.money = oldMoney;
        
        NSLog(@"存50 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
        
    }
    //取钱
    - (void)drawMoney{
        
        int oldMoney = self.money;
        sleep(.2);
        oldMoney -= 20;
        self.money = oldMoney;
        
        NSLog(@"取20 还剩%d元 - %@", oldMoney, [NSThread currentThread]);
        
    }
    
    - (void)saleTicket{
        
        //这里使用oldTicketsCount主要是模拟整个读取票数然后卖票的过程,睡眠0.2使效果更明显
        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];
            }
        });
        
    }
    
    
    @end
    

    然后例如要演示OSSpinLock锁,我们可以创建一个类名为OSSPinLockDemo继承自BaseDemo,然后在其中实现存钱取钱卖票:

    //OSSpinLockDemo.m
    - (instancetype)init{
        
        if (self = [super init]) {
            self.moneyLock = OS_SPINLOCK_INIT;
            self.ticketlock = OS_SPINLOCK_INIT;
        }
        
        return self;
    }
    
    - (void)saveMoney{
        
        OSSpinLockLock(&_moneyLock);
        
        [super saveMoney];
        
        OSSpinLockUnlock(&_moneyLock);
        
    }
    
    - (void)drawMoney{
        
        OSSpinLockLock(&_moneyLock);
        
        [super drawMoney];
        
        OSSpinLockUnlock(&_moneyLock);
    }
    
    - (void)saleTicket{
        
        OSSpinLockLock(&_ticketlock);
        
        [super saleTicket];
        
        OSSpinLockUnlock(&_ticketlock);
    }
    

    在主函数中这样调用:

        OSSpimLinkDemo *demo = [[OSSpimLinkDemo alloc] init];
        [demo ticketTest];
    

    这样做的好处是,我们可以更加专注于加锁的过程,而不用去管业务逻辑,每学习一个锁,就写一个子类。

    os_unfair_lock

    下面学习os_unfair_lock这种锁。

    os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持。
    从底层调用看,等待os_unfair_lock锁的线程处于休眠状态,并非忙等。
    需要导入头文件<os/lock.h>

    os_unfair_lock的基本API如下:

            //初始化
            os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
            //尝试加锁
            os_unfair_lock_trylock(&lock);
            //加锁
            os_unfair_lock_lock(&lock);
            //解锁
            os_unfair_lock_unlock(&lock);
    

    接下来我们可以写一个子类OSUnFairLockDemo类,然后在这个类中重写卖票方法如下:

    //OSUnFairLockDemo.m
    - (instancetype)init{
        
        if (self = [super init]) {
            self.ticketlock = OS_UNFAIR_LOCK_INIT;
        }
        
        return self;
    }
    - (void)saleTicket{
        
        os_unfair_lock_lock(&_ticketlock);
        
        [super saleTicket];
        
        os_unfair_lock_unlock(&_ticketlock);
    }
    

    然后看一下输出结果:

    2018-09-27 16:06:24.453628+0800 TEST[26669:857080] 最后还剩的票数14 线程<NSThread: 0x600002c06100>{number = 3, name = (null)}
    2018-09-27 16:06:24.453777+0800 TEST[26669:857080] 最后还剩的票数13 线程<NSThread: 0x600002c06100>{number = 3, name = (null)}
    2018-09-27 16:06:24.453893+0800 TEST[26669:857080] 最后还剩的票数12 线程<NSThread: 0x600002c06100>{number = 3, name = (null)}
    2018-09-27 16:06:24.453988+0800 TEST[26669:857080] 最后还剩的票数11 线程<NSThread: 0x600002c06100>{number = 3, name = (null)}
    2018-09-27 16:06:24.454108+0800 TEST[26669:857080] 最后还剩的票数10 线程<NSThread: 0x600002c06100>{number = 3, name = (null)}
    2018-09-27 16:06:24.454235+0800 TEST[26669:857082] 最后还剩的票数9 线程<NSThread: 0x600002c00ec0>{number = 4, name = (null)}
    2018-09-27 16:06:24.454323+0800 TEST[26669:857082] 最后还剩的票数8 线程<NSThread: 0x600002c00ec0>{number = 4, name = (null)}
    2018-09-27 16:06:24.454421+0800 TEST[26669:857082] 最后还剩的票数7 线程<NSThread: 0x600002c00ec0>{number = 4, name = (null)}
    2018-09-27 16:06:24.454513+0800 TEST[26669:857082] 最后还剩的票数6 线程<NSThread: 0x600002c00ec0>{number = 4, name = (null)}
    2018-09-27 16:06:24.454600+0800 TEST[26669:857082] 最后还剩的票数5 线程<NSThread: 0x600002c00ec0>{number = 4, name = (null)}
    2018-09-27 16:06:24.454712+0800 TEST[26669:857083] 最后还剩的票数4 线程<NSThread: 0x600002c06180>{number = 5, name = (null)}
    2018-09-27 16:06:24.454840+0800 TEST[26669:857083] 最后还剩的票数3 线程<NSThread: 0x600002c06180>{number = 5, name = (null)}
    2018-09-27 16:06:24.458107+0800 TEST[26669:857083] 最后还剩的票数2 线程<NSThread: 0x600002c06180>{number = 5, name = (null)}
    2018-09-27 16:06:24.458217+0800 TEST[26669:857083] 最后还剩的票数1 线程<NSThread: 0x600002c06180>{number = 5, name = (null)}
    2018-09-27 16:06:24.458307+0800 TEST[26669:857083] 最后还剩的票数0 线程<NSThread: 0x600002c06180>{number = 5, name = (null)}
    

    可以看到,数据没有发生混乱。

    pthread_mutex

    mutex叫做"互斥锁",等待锁的线程会处于休眠状态。
    需要导入头文件<pthread.h>

    与之相关的API有:

            //初始化锁的属性
            pthread_mutexattr_t attr;
            pthread_mutexattr_init(&attr);
            pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
            
            //初始化锁
            pthread_mutex_t mutex;
            pthread_mutex_init(&mutex, &attr);
            
            //尝试加锁
            pthread_mutex_trylock(&mutex);
            //加锁
            pthread_mutex_lock(&mutex);
            //解锁
            pthread_mutex_unlock(&mutex);
            //销毁相关资源
            pthread_mutexattr_destroy(&attr);
            pthread_mutex_destroy(&mutex);
            
            /*
             *Mutex type attributes
             */
            #define PTHREAD_MUTEX_NORMAL       0
            #define PTHREAD_MUTEX_ERRORCHECK   1
            #define PTHREAD_MUTEX_RECURSIVE    2
            #define PTHREAD_MUTEX_DEFAULT
    

    我们可以创建一个子类MutexDemo,然后重写卖票方法:

    //MutexDemo.m
    - (instancetype)init{
        
        if (self = [super init]) {
            
            //初始化锁的属性
            pthread_mutexattr_t attr;
            pthread_mutexattr_init(&attr);
            pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL );
            
            //初始化锁
            pthread_mutex_t mutex;
            pthread_mutex_init(&_ticketLock, &attr);   
            pthread_mutexattr_destroy(&attr);
        }
        
        return self;
    }
    
    - (void)saleTicket{
        
        pthread_mutex_lock(&_ticketLock);
        
        [super saleTicket];
        
        pthread_mutex_unlock(&_ticketLock);
    }
    

    打印出来数据没有发生混乱。

    由一个问题引出递归锁

    创建一个子类MutexDemo2,在这个类中像MutexDemo一样,创建pthread_Mutex类型的互斥锁:

    - (instancetype)init{
        
        if (self = [super init]) {
            
            //初始化锁的属性
            pthread_mutexattr_t attr;
            pthread_mutexattr_init(&attr);
            //通过属性确定创建的是互斥锁
            pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
            
            //初始化锁
            pthread_mutex_init(&_ticketLock, &attr);
            
            pthread_mutexattr_destroy(&attr);
           
        }
        
        return self;
    }
    
    - (void)otherTest{
        
        pthread_mutex_lock(&_ticketLock);
        
        NSLog(@"%s", __func__);
        [self otherTest2];
        
        pthread_mutex_unlock(&_ticketLock);
    }
    
    - (void)otherTest2{
        
        pthread_mutex_lock(&_ticketLock);
        
        NSLog(@"%s", __func__);
        
        pthread_mutex_unlock(&_ticketLock);
    }
    

    然后创建实例对象去调用otherTest这个方法:

        MutexDemo2 *demo = [[MutexDemo2 alloc] init];
        [demo otherTest];
    

    我们看一下运行效果:

    2018-09-27 18:44:56.627062+0800 TEST[30733:965088] -[MutexDemo2 otherTest]
    

    只打印了otherTest方法中的输出,而没有打印otherTest2方法中的输出,这是什么原因呢?
    原因在于,执行otherTest时,将ticketLock这个锁锁上了,锁上后去调用otherTest2方法,在otherTest2方法中,检查到锁锁上了,所以就会一直在碗面等,等这个锁打开,而锁打开又依赖于otherTest2方法执行完成,这样代码就没法执行下去了。
    这个方法其实很好解决,由于是两个不同的方法,所以这两个方法使用不同的锁就行了,那么如果是递归呢?也就是otherTest里面调用otherTest呢?这样就不可能使用两把锁了,那这个问题又该怎么解决呢?
    这个时候递归锁就派上用场了

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

    我们可以把pthread_Mutex锁的属性改为递归锁:

            //改变锁的属性为递归锁
            pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    
    - (void)otherTest{
        //第二次调用到这个地方的时候,可以再给ticketLock这个锁加一次锁
        pthread_mutex_lock(&_ticketLock);
        
        NSLog(@"%s", __func__);
        [self otherTest];
        //在解锁的时候相对应也会解两次锁
        pthread_mutex_unlock(&_ticketLock);
    }
    

    这样就能解决这个递归死锁的问题。

    从汇编实现来看自旋锁是忙等,互斥锁是休眠

    我们在BaseDemo这个基类中修改ticketTest这个方法的实现,创建十条线程来调用saleTicket方法:

    - (void)ticketTest{
        
        self.ticketsCount = 15;
        
        for (int i = 0; i < 15; i++) {
            [[[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil] start];
        }
    

    然后在saleTicket这个方法里面设置睡眠时间为600s,这样一来,当第一条线程进入saleTicket方法后,由于休眠600s,所以锁在600s内会被锁着,当第二条线程调用saleTicket方法时,就会在外面等待:

    - (void)saleTicket{
        
        //睡眠600s是保证第二条线程进来时锁是被锁着,于是w要在外面等待
        int oldTicketsCount = self.ticketsCount;
        sleep(600);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        
        NSLog(@"最后还剩的票数%d 线程%@", oldTicketsCount, [NSThread currentThread]);
    }
    
    为了研究自旋锁,我们选择OSSpinLock这个锁,在OSSpinLock的类文件中打下断点:
    BA891BC4-1435-4202-8EC1-846FFDFD287B.png

    当第一条线程访问时,直接过掉断点,第二条线程执行到断点处时,进入汇编里面查看等待的过程。
    下面是第二条线程执行到断点处时进入汇编:


    E9ABC18A-47E9-40C1-9C76-405A3AE52AF5.png

    我们可以使用stepi指令或者si指令来一步一步执行汇编指令,这样单步执行遇到函数时会调进函数。

    然后我们使用si指令来一步一步执行汇编指令,执行到ox105f329b1时跳进去了,通过si一步一步的执行,最终来到了下面的汇编:

    2673C536-E6B2-4E18-886E-CC30A91051CB.png
    执行的时候发现,汇编指令在0x107ef3a32和0x107ef3a43之间循环执行,jne就是一个while循环,条件满足就继续执行框内的代码,等待条件不满足也就是锁已经打开就继续往下执行。这里也就证明了自旋锁使用的是忙等。
    为了研究互斥锁,我们选择pthread_Mutex这个锁,单步执行很多次之后,跳到了下图:

    采用研究OSSpinLock一样的方法,通过汇编指令来解读

    5A75BF7A-CCB0-47A1-9E95-3A323E4D8566.png
    这个syscall是一个系统级的函数,单步执行到这一步的时候,下一步就是执行这个函数了,执行这一步之后,马上退出了汇编指令的界面,回到了模拟器的界面。这就说明线程产生了休眠,不干事了,所以会退出。这也就说明了互斥锁在等待的时候会线程休眠。
    通过汇编指令判断os_unfair_lock是自旋锁还是互斥锁

    还是通过和前面两个锁一样的方法来查看,单步执行汇编指令,执行到最后到了下面的指令:


    DCDF9627-09A8-42EC-99C4-97F3D6DA1A4D.png

    汇编指令执行到最后还是执行到了syscall这一步,这就说明os_unfair_lock在等待时线程是休眠的,也就证明了其是互斥锁。

    NSLock

    NSLock是对mutex普通锁的封装,所以它是一种互斥锁。

    @interface NSLock : NSObject <NSLocking> {
    - (BOOL)tryLock;
    //在这个时间之前如果能等到这把锁放开,那么就给这把锁加锁,加锁成功,返回YES,如果到了规定的时间这把锁还是没有放开,那就加锁失败,返回NO。
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    @end
    

    其遵循的NSLocking协议如下:

    @protocol NSLocking
    
    - (void)lock;
    - (void)unlock;
    
    @end
    

    因此NSLock使用起来也是非常简单,创建:

    NSLock *lock = [[NSLock alloc] init];
    

    上锁:

    [lock lock];
    

    解锁:

    [lock unlock];
    

    NSRecursiveLock递归锁

    这个锁是对mutex递归锁的封装,也就是mutex锁的属性为PTHREAD_MUTEX_RECURSIVE,这就是NSRecursiveLock锁了,这个锁的API和NSLock基本一致:

    @interface NSRecursiveLock : NSObject <NSLocking> {
    
    - (BOOL)tryLock;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    @end
    

    其同样遵守NSLocking协议。在使用上与NSLock也是基本一致。

    NSCondition

    NSCondition是对mutex和cond的封装
    其主要API如下:

    @interface NSCondition : NSObject <NSLocking> {
    - (void)wait;
    - (BOOL)waitUntilDate:(NSDate *)limit;
    - (void)signal;
    - (void)broadcast;
    @end
    

    下面举一个例子说明其应用:
    有两条线程,一条线程对数组元素进行删除操作,一条进行添加操作。这个时候在做删除操作的时候就要格外小心,因为如果数组为空,进行删除操作就可能引发崩溃,这个时候就可以在删除操作中做个判断,如果元素数为0,那么就等待,线程进入休眠状态。同时,在添加元素的操作中也要做处理,当添加完元素后要发出一个信号,这个信号告诉删除的那条线程可以醒来继续处理了。

    @interface NSConditionDemo()
    
    @property (nonatomic, strong)NSMutableArray *data;
    @property (nonatomic, strong)NSCondition *condition;
    
    @end
    
    @implementation NSConditionDemo
    
    - (instancetype)init{
        
        if (self = [super init]) {
            
            self.data = [[NSMutableArray alloc] init];
            self.condition = [[NSCondition alloc] init];
        }
        
        return self;
    }
    
    - (void)__remove{
        
        [self.condition lock];
        NSLog(@"__rermove - begin");
        
        if (self.data.count == 0) {
            [self.condition wait];
        }
        
        [self.data removeLastObject];
        NSLog(@"删除了元素");
        [self.condition unlock];
    }
    
    - (void)__add{
        
        [self.condition lock];
        sleep(1.0);
        
        [self.data addObject:@"test"];
        [self.condition signal];
        NSLog(@"添加了元素");
        
        [self.condition unlock];
    }
    
    - (void)otherTest{
        
        [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start];
        [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];
    }
    
    @end
    

    首先调用的是remove操作,进入remove后先加锁,然后判断元素个数是否为0,如果是0那就让线程进入休眠,同时放开锁。然后执行add操作,进入add操作后马上加锁,当添加元素完成后就发出信号,这时remove那条线程就会被唤醒,但是由于add操作时加的锁还没有放开,所以remove线程还要等待锁放开才能继续执行,当锁放开后就能执行删除元素的操作了,完成之后就把锁放开。

    dispatch_semaphore

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

    相关API如下:

            //信号量的初始值
            int value = 1;
            //初始化信号量
            dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
            //如果c信号量的值<=0,当前线程就会进入休眠等待(直到信号量的值>0)
            //如果信号量的值>0,就减1,然后往下执行后面的代码
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            //让信号量的值加1
            dispatch_semaphore_signal(semaphore);
    

    下面以一个实例来讲解信号量的用法:
    要创建20条线程,每条线程执行同样的方法,这样20条线程会对同样的代码执行同样的方法,现在要限制同时执行该方法的线程数为5,那么 就可以使用信号量:

    @interface SemaphoreDemo()
    
    @property (strong ,nonatomic)dispatch_semaphore_t sempahore;
    
    @end
    
    @implementation SemaphoreDemo
    
    - (instancetype)init{
        
        if (self = [super init]) {
            
            self.sempahore = dispatch_semaphore_create(5);
        }
        
        return self;
    }
    
    - (void)otherTest{
        
        for (int i = 0; i < 20; i++) {
            [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
        }
    }
    
    - (void)test{
        
        dispatch_semaphore_wait(_sempahore, DISPATCH_TIME_FOREVER);
        //这是为了使效果更明显
        sleep(1);
        NSLog(@"test - %@", [NSThread currentThread]);
        
        dispatch_semaphore_signal(_sempahore);
    }
    
    @end
    

    第一条线程执行test方法时信号量的值是5,在dispatch_semaphore_wait()这里,当信号量>0时会让线程进入,然后信号量减1,当信号量=0时就会让线程在外面等待,直到信号量>0才让线程进入。进入的线程在执行完以后会进入dispatch_semaphore_signal(),这个方法让信号量加1。

    如果要用信号量保证线程同步,只需要使最大并发线程数为1。

    NSConditionLock

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

    具体的API如下:

    @interface NSConditionLock : NSObject <NSLocking> {
    
    - (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
    
    @property (readonly) NSInteger condition;
    - (void)lockWhenCondition:(NSInteger)condition;
    - (BOOL)tryLock;
    - (BOOL)tryLockWhenCondition:(NSInteger)condition;
    - (void)unlockWithCondition:(NSInteger)condition;
    - (BOOL)lockBeforeDate:(NSDate *)limit;
    - (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
    
    @end
    

    比如我有三个任务,任务1,任务2,任务3,我想要让任务1完成后再执行任务2,任务2执行完后再执行任务3,那么这时就可以使用条件锁:

    @interface NSConditionLockDemo()
    @property (nonatomic, strong)NSConditionLock *conditionLock;
    @end
    
    @implementation NSConditionLockDemo
    
    - (instancetype)init{  
        if (self = [super init]) {        
            self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
        } 
        return self;
    }
    
    - (void)task1{
        //当这把锁内部所存储的条件值为1的时候就会进行加锁,否则就会在这里等待
        [self.conditionLock lockWhenCondition:1];
        NSLog(@"任务一");   
        //设置这把锁内部的条件值为2,同时把锁放开
        [self.conditionLock unlockWithCondition:2];    
    }
    
    - (void)task2{
        //当条件值为2且锁放开时加锁
        [self.conditionLock lockWhenCondition:2];   
        NSLog(@"任务二");    
        //设置这把锁内部的i条件值为3,同时把锁放开
        [self.conditionLock unlockWithCondition:3];
    }
    
    - (void)task3{    
        //当条件值为3且锁放开时加锁
        [self.conditionLock lockWhenCondition:3];    
        NSLog(@"任务三");    
        [self.conditionLock unlock];
    }
    
    - (void)otherTest{    
        [[[NSThread alloc] initWithTarget:self selector:@selector(task1) object:nil] start];
        [[[NSThread alloc] initWithTarget:self selector:@selector(task2) object:nil] start];
        [[[NSThread alloc] initWithTarget:self selector:@selector(task3) object:nil] start];
    }
    
    @end
    

    SerialQueue

    线程同步的本质是不能让多条线程占用同一份资源,直接使用GCD的串行队列,也可以实现线程同步
    例如卖票的方法,要让票一张一张的卖,那也可以使用串行队列,把卖票的方法加入串行队列中,这样就能实现一张票卖完了之后才开始卖下一张票。

    @synchronized

    @synchronized是对mutex递归锁的封装
    @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁,加锁操作
    从代码简洁度来看是最简单的方案

    在买票的程序里我们可以这样用@synchronized:

    - (void)saleTicket{
        
        static NSObject *lock;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            lock = [[NSObject alloc] init];
        });
        
        //保证每次传入的是同一个对象
        @synchronized (lock) {
            [super saleTicket];
        }
    }
    

    @synchronized的括号里相当于就是一把锁,这就相当于是给括号里的一把锁上锁,大括号里就是要执行的东西。任何对象都可以传入括号里面当锁,但是为了让大括号内的代码同一时刻只能被执行一次,这就要求每个线程进来时用的锁是一样的,所以这里声明了一个static类型的NSObject对象,并用单例去创建它。

    多线程同步方案性能对比

    性能由高到低排序:

    image.png
    什么情况下使用自旋锁比较划算?
    • 预计线程等待锁的时间很短
    • 加锁的代码经常被调用,但竞争情况很少发生
    • CPU资源不紧张
    • 多核处理器
      什么情况使用互斥锁比较划算?
    • 预计线程等待时间较长
    • 单核处理器
    • 加锁的代码有IO操作(耗性能)

    atomic

    我们都知道,属性修饰符中有nonatomic和atomic,但是我们在申明属性的时候好像用的都是nonatomic而不是atomic,这是为什么呢?atomic又是什么意思呢?

    atomic用于保证属性setter,getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,会进行加锁和解锁。
    可以参考runtime源码的objc-accessors.mm文件。
    它并不能保证使用属性的过程是线程安全的。

    当我们声明一个属性的时候,系统会自动帮我们实现set和get方法,比如我们声明一个NSString类型的name属性,并用nonatomic来修饰,那么其set和get方法的默认实现如下:

    - (NSString *)name{
        
        return _name;
    }
    
    - (void)setName:(NSString *)name{
        
        _name = name;
    }
    

    上面是用nonatomic方法修饰属性,如果是用atomic修饰属性,那么就会在访问属性和设置属性的时候给其加上锁:

    //保证内部的线程同步
    - (NSString *)name{
        //加锁
        return _name;
        //解锁
    }
    
    - (void)setName:(NSString *)name{
        //加锁
        _name = name;
        //解锁
    }
    

    下面我们通过源码来证实一下:
    打开runtime源码的objc-accessors.mm文件,先看取值方法:

    BE2866C9-7169-40C9-ADA1-F512E31D72C3.png
    再看一下设值的方法:
    910C3AD8-6733-40B1-BE3E-675DD33FCBB1.png
    使用atomic确实可以保证set方法和get方法内部是线程安全的,但是它并不能保证使用属性的过程是线程安全的,这句话是什么意思呢?
    比如说有一个data属性:
    @property (atomic, strong)NSMutableArray *data;
    

    那么下列代码是不是线程安全的呢:

            self.data = [[NSMutableArray alloc] init];
            
            [self.data addObject:@"1"];
            [self.data addObject:@"2"];
            [self.data addObject:@"3"];
    

    有人可能会想,这不就是取值和设值的操作吗?就是调用了set和get方法呀,而atomic修饰的属性,其set和get方法是线程安全的呀。上述代码可以等价于下面的:

            [self setData:[[NSMutableArray alloc] init]];
            
            [[self data] addObject:@"1"];
            [[self data] addObject:@"2"];
            [[self data] addObject:@"3"];
    

    问题出就出在,并不是只用了set和get方法,还有addObject方法呀,这可不是线程安全的,加入有多条线程同时执行addObject方法,它就不是安全的了。

    由于set方法和get方法使用的非常多,而如果是用atomic修饰的话,那么每使用一次set或者get方法都会进行加锁和解锁,这样频繁的加锁和解锁是非常耗性能的,并且也不能保证使用属性的过程是线程安全的,因此一般不用atomic,转而用nonatomic。

    相关文章

      网友评论

          本文标题:iOS源码解析:多线程<二>线程同步

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