iOS源码解析:多线程 线程同步

起始票数是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 线程

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

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

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

 {number = 3, name = (null)}2018-09-26 16:45:14.317953+0800 TEST[12223:379269] 存50 还剩200元 - 
 
  {number = 3, name = (null)}2018-09-26 16:45:14.318071+0800 TEST[12223:379269] 存50 还剩250元 - 
  
   {number = 3, name = (null)}2018-09-26 16:45:14.318182+0800 TEST[12223:379269] 存50 还剩300元 - 
   
    {number = 3, name = (null)}2018-09-26 16:45:14.318374+0800 TEST[12223:379269] 存50 还剩350元 - 
    
     {number = 3, name = (null)}2018-09-26 16:45:14.318500+0800 TEST[12223:379269] 存50 还剩400元 - 
     
      {number = 3, name = (null)}2018-09-26 16:45:14.318587+0800 TEST[12223:379269] 存50 还剩450元 - 
      
       {number = 3, name = (null)}2018-09-26 16:45:14.318689+0800 TEST[12223:379269] 存50 还剩500元 - 
       
        {number = 3, name = (null)}2018-09-26 16:45:14.318823+0800 TEST[12223:379269] 存50 还剩550元 - 
        
         {number = 3, name = (null)}2018-09-26 16:45:14.319047+0800 TEST[12223:379269] 存50 还剩600元 - 
         
          {number = 3, name = (null)}2018-09-26 16:45:14.320129+0800 TEST[12223:379270] 取20 还剩580元 - 
          
           {number = 4, name = (null)}2018-09-26 16:45:14.320242+0800 TEST[12223:379270] 取20 还剩560元 - 
           
            {number = 4, name = (null)}2018-09-26 16:45:14.320347+0800 TEST[12223:379270] 取20 还剩540元 - 
            
             {number = 4, name = (null)}2018-09-26 16:45:14.320459+0800 TEST[12223:379270] 取20 还剩520元 - 
             
              {number = 4, name = (null)}2018-09-26 16:45:14.320588+0800 TEST[12223:379270] 取20 还剩500元 - 
              
               {number = 4, name = (null)}2018-09-26 16:45:14.320693+0800 TEST[12223:379270] 取20 还剩480元 - 
               
                {number = 4, name = (null)}2018-09-26 16:45:14.320900+0800 TEST[12223:379270] 取20 还剩460元 - 
                
                 {number = 4, name = (null)}2018-09-26 16:45:14.321222+0800 TEST[12223:379270] 取20 还剩440元 - 
                 
                  {number = 4, name = (null)}2018-09-26 16:45:14.321331+0800 TEST[12223:379270] 取20 还剩420元 - 
                  
                   {number = 4, name = (null)}2018-09-26 16:45:14.321548+0800 TEST[12223:379270] 取20 还剩400元 - 
                   
                    {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_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 线程

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

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

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态。

需要导入头文件

与之相关的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的类文件中打下断点: