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

多线程的安全隐患

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


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。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容