[iOS] 多线程安全

1. 什么情况下会有线程隐患?

我们在使用多线程技术带来的便利的同时,也需要考虑下多线程所带来的隐患。比如,我们可以并发的进行多个任务,因此同一块资源就可能在多个线程中同时被访问(读/写),这个现象叫做资源共享,比如多个线程同时访问了同一个对象\变量\文件,这样就有可能引发数据错乱数据安全问题。

2. 两个常见的示例

2.1 示例:存钱取钱
存钱取钱问题

我们用代码来实现一下这个功能:

- (void)moneyTest{
    self.money = 1000;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self drawMoney];
        }
    });
}

// 存钱
- (void)saveMoney{
    NSInteger oldMoney = self.money;
    sleep(0.5);
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存了50元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
}

// 取钱
- (void)drawMoney{
    NSInteger oldMoney = self.money;
    sleep(0.5);
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取了20元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
}

输出结果:

2019-10-12 16:40:18.033330+0800 tianran[16615:4423457] 存了50元,账户余额 1050,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033486+0800 tianran[16615:4423457] 存了50元,账户余额 1080,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033500+0800 tianran[16615:4423456] 取了20元,账户余额 1030,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033573+0800 tianran[16615:4423457] 存了50元,账户余额 1130,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033632+0800 tianran[16615:4423456] 取了20元,账户余额 1110,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033659+0800 tianran[16615:4423457] 存了50元,账户余额 1160,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033730+0800 tianran[16615:4423456] 取了20元,账户余额 1140,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033737+0800 tianran[16615:4423457] 存了50元,账户余额 1190,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033821+0800 tianran[16615:4423457] 存了50元,账户余额 1240,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.033839+0800 tianran[16615:4423456] 取了20元,账户余额 1170,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.033949+0800 tianran[16615:4423457] 存了50元,账户余额 1220,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034040+0800 tianran[16615:4423456] 取了20元,账户余额 1200,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034178+0800 tianran[16615:4423457] 存了50元,账户余额 1250,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034341+0800 tianran[16615:4423456] 取了20元,账户余额 1230,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034423+0800 tianran[16615:4423457] 存了50元,账户余额 1280,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034581+0800 tianran[16615:4423456] 取了20元,账户余额 1260,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.034665+0800 tianran[16615:4423457] 存了50元,账户余额 1310,当前线程 <NSThread: 0x2829d1840>{number = 3, name = (null)}
2019-10-12 16:40:18.034803+0800 tianran[16615:4423456] 取了20元,账户余额 1290,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.035029+0800 tianran[16615:4423456] 取了20元,账户余额 1270,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}
2019-10-12 16:40:18.035105+0800 tianran[16615:4423456] 取了20元,账户余额 1250,当前线程 <NSThread: 0x2829d91c0>{number = 4, name = (null)}

我们在moneyTest方法中,以多线程方式分别进行了10次的存/取钱操作,每次存50,每次取20,最后执行完之后余额应该为1000 + (50 * 10) - (20 * 10) = 1300,但是上面输出的结果是1250,很明显,出大问题了。

2.2 示例:卖票问题
卖票问题

我们通过代码展示一下这个问题:

- (void)ticketTest{
    self.ticketNum = 30;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 卖票操作
- (void)saleTicket{
    self.ticketNum -= 1;
    NSLog(@"还剩%ld张票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
}

输出结果:

2019-10-12 16:46:38.501449+0800 tianran[16620:4425096] 还剩29张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501573+0800 tianran[16620:4425094] 还剩28张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501593+0800 tianran[16620:4425096] 还剩27张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501684+0800 tianran[16620:4425096] 还剩25张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501693+0800 tianran[16620:4425094] 还剩24张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501689+0800 tianran[16620:4425093] 还剩26张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.501753+0800 tianran[16620:4425096] 还剩23张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.501776+0800 tianran[16620:4425094] 还剩22张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501781+0800 tianran[16620:4425093] 还剩21张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.501877+0800 tianran[16620:4425094] 还剩19张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.501819+0800 tianran[16620:4425096] 还剩20张票,currentThread - <NSThread: 0x28335a9c0>{number = 5, name = (null)}
2019-10-12 16:46:38.502044+0800 tianran[16620:4425093] 还剩18张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.502404+0800 tianran[16620:4425094] 还剩17张票,currentThread - <NSThread: 0x2833530c0>{number = 3, name = (null)}
2019-10-12 16:46:38.502529+0800 tianran[16620:4425093] 还剩16张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}
2019-10-12 16:46:38.502760+0800 tianran[16620:4425093] 还剩15张票,currentThread - <NSThread: 0x28336aa40>{number = 6, name = (null)}

虽然最后的结果是一样的,但是我们可以看到中间卖票的时候,票数有问题了。

上面两个问题都是由于多个线程对同一资源进行了读写操作而导致的,下面用一个熟悉的图片来表示下:

image.png

针对这个问题的解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)。常见的线程同步技术就是:加锁

如下图所示:


image.png

在进行操作的时候,先加锁,保证此时只有一个线程对资源进行操作,操作完成后在解锁。

3. 线程同步(加锁)方案

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

借鉴一张锁的性能数据对比图,如下所示:

image.jpeg

介绍锁之前,我们可以先看几个概念定义:

  • 临界区:
    指的是一块对公共资源进行访问的代码,并非一种机制或算法。

  • 自旋锁:
    用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式的释放自旋锁。自旋锁避免了线程上下文的调度开销,因此对于线程只会阻塞很短时间的场景是非常有效的,如上面的 OSSpinLockatomic

  • 互斥锁(Mutex):
    是一种用于多线程编程中,防止多个线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成,如上面的@ synchronized、NSLock 、pthread_mutex

  • 条件锁
    条件锁就是条件变量,当进程的某些资源要求不满足时就进入休眠,即锁住了,当资源被分配到了,条件锁打开了,进程继续运行,如上面的NSConditionNSConditionLock

  • 递归锁
    递归锁就是同一个线程可以加锁 N 次而不会引发死锁。递归锁是特殊的互斥锁,即带有递归性质的互斥锁,如:pthread_mutex(recursive)NSRecursiveLock

  • 读写锁:
    是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”或者“多读-单写锁”,是一种特殊的自旋锁。用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。读写锁通常用互斥锁、条件变量、信号量实现。

  • 信号量(semaphore):
    是一种更高级的同步机制互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥,如:dispatch_semaphore

其实基本的锁就包括三类:自旋锁、互斥锁、读写锁,其他的比如条件锁、递归锁、信号量都是上层的封装和实现。

3.1 OSSpinLock

OSSpinLock叫做“自旋锁”,需要导入头文件

#import <libkern/OSAtomic.h>

常用的API:

OSSpinLock lock = OS_UNFAIR_LOCK_INIT; ——初始化锁对象lock
OSSpinLockTry(&lock);——尝试加锁,加锁成功继续,加锁失败返回,继续执行后面的代码,不阻塞线程
OSSpinLockLock(&lock);——加锁,加锁失败会阻塞线程,进行等待
OSSpinLockUnlock(&lock);——解锁

我们使用上面的第二个卖票示例来进行加锁操作:

#import <libkern/OSAtomic.h>
@interface ViewController () 
// 票总数
@property (nonatomic, assign) NSInteger ticketNum;
// 锁
@property (nonatomic, assign) OSSpinLock lock;
@end

- (void)ticketTest{
    
    // 初始化锁
    self.lock = OS_SPINLOCK_INIT;
    self.ticketNum = 30;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 卖票操作
- (void)saleTicket{
    
    // 加锁🔐🔐
    OSSpinLockLock(&_lock);
    
    self.ticketNum -= 1;
    NSLog(@"还剩%ld张票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
    
    // 解锁🔓🔓
    OSSpinLockUnlock(&_lock);
}

结果输出:

2019-10-12 17:02:55.774748+0800 tianran[16628:4429441] 还剩29张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.774955+0800 tianran[16628:4429445] 还剩28张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775065+0800 tianran[16628:4429445] 还剩27张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775156+0800 tianran[16628:4429443] 还剩26张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775237+0800 tianran[16628:4429443] 还剩25张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775301+0800 tianran[16628:4429443] 还剩24张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775368+0800 tianran[16628:4429443] 还剩23张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775433+0800 tianran[16628:4429443] 还剩22张票,currentThread - <NSThread: 0x28349a540>{number = 3, name = (null)}
2019-10-12 17:02:55.775513+0800 tianran[16628:4429445] 还剩21张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775578+0800 tianran[16628:4429445] 还剩20张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775643+0800 tianran[16628:4429445] 还剩19张票,currentThread - <NSThread: 0x28341c080>{number = 6, name = (null)}
2019-10-12 17:02:55.775717+0800 tianran[16628:4429441] 还剩18张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775781+0800 tianran[16628:4429441] 还剩17张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775911+0800 tianran[16628:4429441] 还剩16张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}
2019-10-12 17:02:55.775983+0800 tianran[16628:4429441] 还剩15张票,currentThread - <NSThread: 0x2834ed5c0>{number = 5, name = (null)}

这里要注意,我们使用的是同一个锁对象,如果用多个,肯定也没效果啊。

卖票的问题,我们针对的是同一个操作来处理的,而存钱取钱的问题,涉及到了两个操作(存钱和取钱),首先要明确问题,加锁机制是为了解决多个线程同时访问共享资源所产生的数据问题,无论这些线程里面执行的是什么操作,如果这些操作影响了共享资源,不能同时进行的话,那么应该对这些操作使用同一个锁对象进行加锁。
所以,我们应该对存钱操作取钱操作使用相同的锁对象进行加锁。
代码如下:

#import <libkern/OSAtomic.h>
@interface ViewController () 
// 总钱数
@property (nonatomic, assign) NSInteger money;
// 锁
@property (nonatomic, assign) OSSpinLock lock;
@end

- (void)moneyTest{
    
    // 初始化锁
    self.lock = OS_SPINLOCK_INIT;
    self.money = 1000;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self saveMoney];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 10; i++) {
            [self drawMoney];
        }
    });
}

// 存钱
- (void)saveMoney{
    sleep(0.5);
    OSSpinLockLock(&_lock);
    NSInteger oldMoney = self.money;
    oldMoney += 50;
    self.money = oldMoney;
    NSLog(@"存了50元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
    OSSpinLockUnlock(&_lock);
}

// 取钱
- (void)drawMoney{
    sleep(0.5);
    OSSpinLockLock(&_lock);
    NSInteger oldMoney = self.money;
    oldMoney -= 20;
    self.money = oldMoney;
    NSLog(@"取了20元,账户余额 %ld,当前线程 %@",self.money,[NSThread currentThread]);
    OSSpinLockUnlock(&_lock);
}

输出结果:

2019-10-12 17:14:44.362398+0800 tianran[16653:4433835] 存了50元,账户余额 1050,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362540+0800 tianran[16653:4433832] 取了20元,账户余额 1030,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.362640+0800 tianran[16653:4433835] 存了50元,账户余额 1080,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362711+0800 tianran[16653:4433832] 取了20元,账户余额 1060,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.362790+0800 tianran[16653:4433835] 存了50元,账户余额 1110,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.362923+0800 tianran[16653:4433832] 取了20元,账户余额 1090,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363015+0800 tianran[16653:4433835] 存了50元,账户余额 1140,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363078+0800 tianran[16653:4433832] 取了20元,账户余额 1120,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363152+0800 tianran[16653:4433835] 存了50元,账户余额 1170,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363206+0800 tianran[16653:4433832] 取了20元,账户余额 1150,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363278+0800 tianran[16653:4433835] 存了50元,账户余额 1200,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363335+0800 tianran[16653:4433832] 取了20元,账户余额 1180,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363405+0800 tianran[16653:4433835] 存了50元,账户余额 1230,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363458+0800 tianran[16653:4433832] 取了20元,账户余额 1210,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.363555+0800 tianran[16653:4433835] 存了50元,账户余额 1260,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363723+0800 tianran[16653:4433835] 存了50元,账户余额 1310,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363770+0800 tianran[16653:4433835] 存了50元,账户余额 1360,当前线程 <NSThread: 0x28118a780>{number = 5, name = (null)}
2019-10-12 17:14:44.363889+0800 tianran[16653:4433832] 取了20元,账户余额 1340,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.364014+0800 tianran[16653:4433832] 取了20元,账户余额 1320,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}
2019-10-12 17:14:44.364129+0800 tianran[16653:4433832] 取了20元,账户余额 1300,当前线程 <NSThread: 0x281189080>{number = 3, name = (null)}

这次就没问题了,过程和结果都没有问题。

上面有提到,OSSpinLock自旋锁,那么为什么叫做自旋锁呢?
自旋锁的原理是当已经被别的线程加锁了,加锁失败的时候,让线程处于忙等的状态,以此让线程停留在临界区(需要加锁的代码段)之外,一旦加锁成功,线程便可以进入临界区进行对共享资源操作。

让线程阻塞有两种方法:

  • 让线程休眠,RunLoop里面用到的mach_msg()实现的效果就是这一种,它借助系统内核指令,🙆线程停下来,CPU不再分配资源给线程,因此不会再执行任何一句汇编指令,我们后面要介绍的互斥所,也是属于这种,它的底层汇编调用了一个系统函数syscall使得线程进入休眠
  • 自旋锁的忙等,本质上是一个while循环,不断的去判断加锁条件,一旦当前已经进入临界区(加锁代码块)的线程完成了操作,解开锁之后,等待锁的线程便可以成功加锁,再次进入临界区。自旋锁其实并没有真正让线程停下来,线程只不过是暂时被困在while循环里面,CPU还是在不断的分配资源去处理它的汇编指令的(while循环的汇编指令)。

现在苹果已经建议开发者停止使用OSSpinLock了,在线程优先级的作用下,会产生【优先级反转】,使得自旋锁卡住,因此它不再安全了。
可以看不再安全的OSSpinLock

我们知道,计算机的CPU在同一时间,只能处理一条线程,对于单核``CPU来说,线程的并发,实际上是一种假象,是系统让CPU一很小的时间间隔在线程之间来回切换,所以看上去多条线程好像是在同时进行的。到了多核CPU时代,确实可以实现真正的线程并发,但是CPU核心数毕竟是有限的,而程序内部的线程数量通常肯定是远大于CPU数量的,因此,很多情况下我们面对的还是单CPU处理多线程的情况。基于这种场景,需要了解一个概念叫做线程优先级,CPU会将尽可能多的时间(资源)分配给优先级高的线程,我们用下图来展示一下所谓的优先级反转问题:

image.png

低优先级线程A获得锁并访问共享资源,这时一个高优先级线程B也尝试获得这个锁,它会处于spin lock的忙等状态从而占用大量CPU,此时低优先级的线程A无法与高优先级线程B争夺CPU时间,从而导致线程A任务迟迟完不成,无法释放lock,所以会出现卡住的问题。

自旋锁的while循环本质,使得线程并没有停下来,一般情况下,一条线程等待锁的时间不会太长,选用自旋锁来阻塞线程所消耗的CPU资源,要小于线程的休眠和唤醒所带来的CPU资源开销,因此自选锁是一种效率很高的加锁机制,但是优先级反转问题使得自旋锁不再安全,锁的最终目的是安全不是效率,所以苹果放弃了OSSpinLock

iOS 10/macOS 10.12发布时,苹果提供了新的 os_unfair_lock 作为 OSSpinLock 的替代,并且将OSSpinLock 标记为了 Deprecated

另外为什么RunLoop要选择真正的线程休眠呢?因为对于App来说,可能处于长时间的搁置状态,而没有任何用户行为发生,不需要CPU管,对于这种场景,当然是让线程休眠更为节约性能。

3.2 os_unfair_lock

苹果建议从iOS10.0之后,使用os_unfair_lock代替OSSpinLock,现在我们就去看一下如何使用:
先导入头文件

#import <os/lock.h>

使用的API:

os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
初始化锁对象lock
os_unfair_lock_trylock(&lock);
尝试加锁,加锁成功继续,加锁失败返回,继续执行后面的代码,不阻塞线程
os_unfair_lock_lock(&lock);
加锁,加锁失败会阻塞线程进行等待
os_unfair_lock_unlock(&lock);
解锁

它的使用和OSSpinLock的方法一样,这里就不再举例了,苹果为了解决OSSpinLock的优先级反转问题,在os_unfair_lock中摒弃了忙等方式,使用让线程真正休眠的方式,来阻塞线程,也就从根本上解决了问题。

3.3 atomic

atomic适用于OC中属性的修饰符,其自带一把互斥锁,但是这个一般基本不使用,都是使用的nonatomic

setter 方法会根据修饰符调用不同的方法,其中最后会统一调用 reallySetProperty方法,其中就有对于atomic非 atomic 的操作:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
   ...
   id *slot = (id*) ((char*)self + offset);
   ...

    if (!atomic) {//未加锁
        oldValue = *slot;
        *slot = newValue;
    } else {//加锁
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    ...
}

从源码中可以看出,对于atomic修饰的属性,进行了spinlock_t加锁处理,但是在前文中提到OSSpinLock已经废弃了,这里的spinlock_t在底层是通过os_unfair_lock互斥锁替代了OSSpinLock自旋锁:

using spinlock_t = mutex_tt<LOCKDEBUG>;

class mutex_tt : nocopy_t {
    os_unfair_lock mLock;
    ...
}
3.4 pthread_mutex

pthread_mutex来自pthread,是一个跨平台的解决方案,mutex意为互斥锁,等待锁的线程会处于休眠状态,它有如下API:

需要先导入 #import <pthread.h>

初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NOMAL);
初始化锁
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(&attr);

我们先介绍一下pthread_mutex的初始化方法:

int pthread_mutex_init(pthread_mutex_t * __restrict,
        const pthread_mutexattr_t * _Nullable __restrict);
  • pthread_mutex_t * __restrict
    要进行初始化的锁对象
  • const pthread_mutexattr_t * _Nullable __restrict
    锁对象的属性

由于第二个参数是锁对象的属性,所以我们还需要专门生成属性对象,通过 定义属性对象 -> 初始化属性对象 -> 设置属性种类 3步来完成,属性的类别有以下几类:

#define PTHREAD_MUTEX_NORMAL        0 // 普通互斥锁
#define PTHREAD_MUTEX_ERRORCHECK    1 // 检查错误锁,不常用
#define PTHREAD_MUTEX_RECURSIVE     2 // 递归互斥锁,下面会有介绍
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL

如果我们给锁设定默认属性,那么可以用以下代码进行锁的初始化,不用再配置属性信息,其中参数NULL表示的就是初始化一个普通的互斥锁

pthread_mutex_init(mutex, NULL);

好了,我们先用卖票示例来使用一下pthread_mutex,代码如下:

#import <pthread.h>
@interface ViewController () 
@property (nonatomic, assign) NSInteger ticketNum;
@property (nonatomic, assign) pthread_mutex_t pthread_lock;
@end

- (void)ticketTest{
    
    // 初始化锁的属性
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
    
    // 初始化锁
    pthread_mutex_init(&_pthread_lock, &attr);
    
    self.ticketNum = 30;

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i < 5; i++) {
            [self saleTicket];
        }
    });
}

// 卖票操作
- (void)saleTicket{
    
    // 加锁🔐🔐
    pthread_mutex_lock(&_pthread_lock);
    
    self.ticketNum -= 1;
    NSLog(@"还剩%ld张票,currentThread - %@",self.ticketNum,[NSThread currentThread]);
    
    // 解锁🔓🔓
    pthread_mutex_unlock(&_pthread_lock);
}

-(void)dealloc{
    pthread_mutex_destroy(&_pthread_lock);
}

输出结果就不贴了,是没有问题的,太占地方了。

3.5 互斥递归锁

请看下面的代码场景:

-(void)otherTest {
    NSLog(@"%s",__func__);
    [self otherTest2];    
}

-(void)otherTest2 { 
    NSLog(@"%s",__func__);
}

如果正常调用 otherTest 这个方法,结果如下:

[ViewController otherTest]
[ViewController otherTest2]

如果这两段代码都需要保证线程安全,我们通过加互斥锁,来看下效果:

-(void)otherTest {
    //初始化属性
    pthread_mutexattr_init(&_attr);
    pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
    //初始化锁
    pthread_mutex_init(&_mutex, &_attr);
    
    pthread_mutex_lock(&_mutex);
    NSLog(@"%s",__func__);
    [self otherTest2];
    pthread_mutex_unlock(&_mutex);
    
}

-(void)otherTest2 {
    pthread_mutex_lock(&_mutex);
    NSLog(@"%s",__func__);
    pthread_mutex_unlock(&_mutex);
}

输出结果:

-[ViewController otherTest]

像上面的代码一样,两个方法都加上同一把锁,可以看到调用otherTest方法会导致线程卡在该方法里面,只完成了打印代码的执行,就不继续往下走了,为啥呢?看下图:

死锁

我们可以从上面代码中看到,在otherTest方法中,先加锁了, [self otherTest2] 在加锁之后调用,但是otherTest2方法中,此时还是加锁的状态,所以otherTest2方法一直不执行,由于otherTest方法中,解锁操作是在otherTest2方法执行之后,所以造成死锁。

要解决这个问题很简单,我们分别给两个方法加上不同的锁对象就可以解决了。

但是,如果在开发中如果碰到需要给 递归函数 加锁,如下面所示:

-(void)otherTest {
    pthread_mutex_lock(&_mutex);
    
    NSLog(@"%s",__func__);
    //业务逻辑

    [self otherTest];
    
    pthread_mutex_unlock(&_mutex);
}

这样就无法通过不同的锁对象来加锁了,只要使用相同的锁对象,就会出现死锁。针对这种情况,ptherad给我们提供了递归锁来解决这个问题。要想使用递归锁,我们只需要在初始化属性的时候,选择递归锁属性即可,其他的使用步骤跟普通互斥锁没有区别,如下:

pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_RECURSIVE);

那么递归锁是如何避免死锁的呢?对于同一个锁对象来说,允许重复的加锁,重复的解锁,因为对于一个有出口的递归函数来说:
函数的调用次数 = 函数的退出次数
因此加锁次数pthread_mutex_lock和解锁的次数pthread_mutex_unlock是相等的,所以递归函数结束时,所有的锁都会被解开。

但是递归锁只是针对在同一个线程里面可以重复加锁和解锁。

3.6 互斥条件锁 pthread_cond_t
pthread_mutex_t mutex;  定义一个锁对象
pthread_mutex_init(&mutex, NULL); 初始化锁对象
pthread_cond_t condition; 定义一个条件对象
pthread_cond_init(&condition, NULL); 初始化条件对象
pthread_cond_wait(&condition, &mutex); 等待条件
pthread_cond_signal(&condition); 激活一个等待该条件的线程
pthread_cond_broadcast(&condition); 激活所有等待条件的线程
pthread_mutex_destroy(&mutex); 销毁锁对象
pthread_cond_destroy(&condition); 销毁条件对象

为了解释互斥锁条件的作用,我们来设计一种场景案例:

  • 在 remove 方法里对数组 dataArr 进行删除元素操作
  • 在 add 方法里面对dataArr 进行元素添加操作
  • 并且要求,如果 dataArr 的元素个数为0,则不能进行删除操作
    代码如下:
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *dataArr;
// 锁对象
@property (nonatomic, assign) pthread_mutex_t pthread_lock;
// 条件对象
@property (nonatomic, assign) pthread_cond_t cond;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化属性
    pthread_mutexattr_t _attr;
    pthread_mutexattr_init(&_attr);
    pthread_mutexattr_settype(&_attr, PTHREAD_MUTEX_NORMAL);
    // 初始化锁
    pthread_mutex_init(&_pthread_lock, &_attr);
    // 初始化条件
    pthread_cond_init(&_cond, NULL);
    pthread_mutexattr_destroy(&_attr);
    // 初始化数组
    self.dataArr = [NSMutableArray array];
    
    [self dataArrTest];
}

- (void)dataArrTest{
    // 开启remove 操作,此时dataArr为空
    NSThread *removeThread = [[NSThread alloc] initWithTarget:self selector:@selector(remove) object:nil];
    [removeThread setName:@"RemoveThread"];
    [removeThread start];
    
    // 开启add操作
    NSThread *addThread = [[NSThread alloc] initWithTarget:self selector:@selector(add) object:nil];
    [addThread setName:@"AddThread"];
    [addThread start];
}

// 移除元素
- (void)remove{
    // 加锁
    pthread_mutex_lock(&_pthread_lock);
    
    NSLog(@"加锁成功,线程:%@,remove操作开始",[NSThread currentThread]);
    
    if (!self.dataArr.count) {
        // 进行条件等待
        NSLog(@"dataArr没有元素,开始等待");
        pthread_cond_wait(&_cond, &_pthread_lock);
        NSLog(@"接收到条件更新信号,dataArr有了元素,继续remove操作");
    }
    [self.dataArr removeLastObject];
    NSLog(@"remove成功,dataArr剩余元素个数:%ld",self.dataArr.count);
    
    // 解锁
    pthread_mutex_unlock(&_pthread_lock);
    NSLog(@"remove 解锁成功,线程:%@,线程结束\n",[NSThread currentThread]);
}

// 添加元素
- (void)add{
    // 加锁
    pthread_mutex_lock(&_pthread_lock);
    
    NSLog(@"加锁成功,线程:%@,add操作开始",[NSThread currentThread]);
    sleep(2);
    [self.dataArr addObject:@"111"];
    NSLog(@"add成功,dataArr剩余元素个数:%ld,发送条件信号",self.dataArr.count);
    
    // 发送条件信号
    pthread_cond_signal(&_cond);
    
    // 解锁
    pthread_mutex_unlock(&_pthread_lock);
    NSLog(@"add 解锁成功,线程:%@,线程结束",[NSThread currentThread]);
}

-(void)dealloc{
    pthread_mutex_destroy(&_pthread_lock);
}
@end

输出结果:

2019-10-14 15:14:52.929491+0800 tianran[2004:431723] 加锁成功,线程:<NSThread: 0x283f8cd40>{number = 6, name = RemoveThread},remove操作开始
2019-10-14 15:14:52.929557+0800 tianran[2004:431723] dataArr没有元素,开始等待
2019-10-14 15:14:52.929641+0800 tianran[2004:431724] 加锁成功,线程:<NSThread: 0x283f8c9c0>{number = 7, name = AddThread},add操作开始
2019-10-14 15:14:54.935014+0800 tianran[2004:431724] add成功,dataArr剩余元素个数:1,发送条件信号
2019-10-14 15:14:54.935672+0800 tianran[2004:431723] 接收到条件更新信号,dataArr有了元素,继续remove操作
2019-10-14 15:14:54.935790+0800 tianran[2004:431724] add 解锁成功,线程:<NSThread: 0x283f8c9c0>{number = 7, name = AddThread},线程结束
2019-10-14 15:14:54.936061+0800 tianran[2004:431723] remove成功,dataArr剩余元素个数:0
2019-10-14 15:14:54.936415+0800 tianran[2004:431723] remove 解锁成功,线程:<NSThread: 0x283f8cd40>{number = 6, name = RemoveThread},线程结束

从案例以及运行结果分析,互斥锁的条件pthread_cond_t可以在线程加锁之后,如果条件不达标,暂停线程,等到条件符合标准,继续执行线程,请看下图:


image.png

总结一下pthread_cond_t的作用:

  • 首先在A线程内碰到业务逻辑无法往下执行的时候,调用pthread_cond_wait(),这句代码首先会解锁当前线程,然后休眠当前线程以等待条件信号
  • 此时,锁已经解开,那么值钱等待锁的B线程可以成功加锁,执行它后面的逻辑,由于B线程内的某些操作完成后可以出发A的运行条件,此时从B线程通过pthread_cond_signal(&_cond)向外发出条件信号
  • A线程的收到了条件信号就会被pthread_cond_t唤醒,一旦B线程解锁之后,pthread_cond_t会在A线程内重新加锁,继续A线程的后续操作,并最终解锁。从前到后,有三次加锁,三次解锁
  • 通过pthread_cond_t就实现了一种线程与线程之间的依赖关系,实际开发中我们会有不少场景需要用到这种跨线程依赖关系
3.7 NSLock NSRecursiveLock NSCondition

上面我们了解的 mutex普通锁mutex递归锁mutex条件锁,都是基于C语言的API,苹果在此基础上,进行了一层面向对象的封装:

  • NSLock 封装了pthread_mutex_t (attr = 普通)
  • NSRecursiveLock封装了pthread_mutex_t (attr = 递归)
  • NSCondition 封装了pthread_mutex_t + pthread_cond_t

由于底层是pthread_mutex,这里不再通过代码案例演示,下面列举一下相关API使用方法:

//普通锁
NSLock *lock = [[NSLock alloc] init];
[lock lock];
[lock unlock];
//递归锁
NSRecursiveLock *rec_lock = [[NSRecursiveLock alloc] 
[rec_lock lock];
[rec_lock unlock];init];
//条件锁
NSCondition *condition = [[NSCondition alloc] init];
[self.condition lock];
[self.condition wait];
[self.condition signal];
[self.condition unlock];
3.8 NSConditionLock

苹果基于NSCondition又进一步封装了NSConditionLock,该锁允许我们在锁中设定条件具体条件值,有了这个功能,我们可以更加方便的管理多条线程的依赖关系和前后执行顺序,首先看一下相关API:

- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

特色功能:

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;

接下来通过案例来说明它的功能:

@interface ViewController ()

@property (nonatomic, strong) NSConditionLock *conditionLock;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.conditionLock = [[NSConditionLock alloc] init];
    
    [self dataArrTest];
}

- (void)dataArrTest{

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        [self.conditionLock lock];
        NSLog(@"__one");
        sleep(1);
        [self.conditionLock unlockWithCondition:2];
    });
    
    dispatch_async(queue, ^{
        [self.conditionLock lockWhenCondition:2];
        NSLog(@"__two");
        sleep(1);
        [self.conditionLock unlockWithCondition:3];
    });
    
    dispatch_async(queue, ^{
        [self.conditionLock lockWhenCondition:3];
        NSLog(@"__three");
        sleep(1);
        [self.conditionLock unlock];
    });
}
@end

代码输出:

__one __two __three

全局并发队列,异步操作,这里就是使用NSConditionLock达到了线程依赖的功能,使用NSOperation也可以,我们用图说明一下:


image.png
3.9 dispatch_semaphore

GCD提供了dispatch_semaphore方案来处理多线程同步问题。

3.10 @synchronized

这个使用很简单,也比较常见:

@synchronized (lockObj) {
        /*
         加锁代码(临界区)
         */
    }

但是它是所有线程同步方案里面性能最差的。

开启汇编调试,发现@synchronized在执行过程中,会走底层的 objc_sync_enterobjc_sync_exit方法:

image.png

也可以通过 clang,查看底层编译代码:


image.jpeg

通过对objc_sync_enter方法进行符号断点,查看底层所在的源码库,通过断点发现在objc 源码中,进入objc_sync_enter的源码实现:

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {//传入不为nil
        SyncData* data = id2data(obj, ACQUIRE);//重点
        ASSERT(data);
        data->mutex.lock();//加锁
    } else {//传入nil
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }

    return result;
}

主要操作是判断 obj 有值,然后通过id2data获取了相应的 SyncData,然后进行了加锁操作。

我们再看下objc_sync_exit源码实现:

// End synchronizing on 'obj'. 结束对“ obj”的同步
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    
    if (obj) {//obj不为nil
        SyncData* data = id2data(obj, RELEASE); 
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();//解锁
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {//obj为nil时,什么也不做
        // @synchronized(nil) does nothing
    }
    return result;
}

这个方法在获取了对应的 SyncData 之后,进行了解锁操作。

通过上面两个实现逻辑发现,在 obj 存在时,都会通过id2data方法,获取SyncData

进入 SyncData,是一个结构体:

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

其内部有一个结构体的指针struct SyncData* nextData,类似于链表结构,有 next指向,并且封装了recursive_mutex_t mutex,确认@synchronized是一个递归互斥锁

进入SyncCache定义,也是一个结构体,用于存储线程,其中 list[0]表示当前线程的链表 data,主要用于存储 SyncData 和 lockCount:

typedef struct {
    SyncData *data;
    unsigned int lockCount;  // number of times THIS THREAD locked this block
} SyncCacheItem;

typedef struct SyncCache {
    unsigned int allocated;
    unsigned int used;
    SyncCacheItem list[0];
} SyncCache;

我们现在来看下主要的id2data方法,这个是加锁和解锁都会调用的方法:

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if SUPPORT_DIRECT_THREAD_KEYS //tls(Thread Local Storage,本地局部的线程缓存)
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    //通过KVC方式对线程进行获取 线程绑定的data
    SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY);
    //如果线程缓存中有data,执行if流程
    if (data) {
        fastCacheOccupied = YES;
        //如果在线程空间找到了data
        if (data->object == object) {
            // Found a match in fast cache.
            uintptr_t lockCount;

            result = data;
            //通过KVC获取lockCount,lockCount用来记录 被锁了几次,即 该锁可嵌套
            lockCount = (uintptr_t)tls_get_direct(SYNC_COUNT_DIRECT_KEY);
            if (result->threadCount <= 0  ||  lockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                //objc_sync_enter走这里,传入的是ACQUIRE -- 获取
                lockCount++;//通过lockCount判断被锁了几次,即表示 可重入(递归锁如果可重入,会死锁)
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);//设置
                break;
            }
            case RELEASE:
                //objc_sync_exit走这里,传入的why是RELEASE -- 释放
                lockCount--;
                tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)lockCount);
                if (lockCount == 0) {
                    // remove from fast cache
                    tls_set_direct(SYNC_DATA_DIRECT_KEY, NULL);
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);//判断缓存中是否有该线程
    //如果cache中有,方式与线程缓存一致
    if (cache) {
        unsigned int I;
        for (i = 0; i < cache->used; i++) {//遍历总表
            SyncCacheItem *item = &cache->list[I];
            if (item->data->object != object) continue;

            // Found a match.
            result = item->data;
            if (result->threadCount <= 0  ||  item->lockCount <= 0) {
                _objc_fatal("id2data cache is buggy");
            }
                
            switch(why) {
            case ACQUIRE://加锁
                item->lockCount++;
                break;
            case RELEASE://解锁
                item->lockCount--;
                if (item->lockCount == 0) {
                    // remove from per-thread cache 从cache中清除使用标记
                    cache->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    OSAtomicDecrement32Barrier(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }

    // Thread cache didn't find anything.
    // Walk in-use list looking for matching object
    // Spinlock prevents multiple threads from creating multiple 
    // locks for the same new object.
    // We could keep the nodes in some hash table if we find that there are
    // more than 20 or so distinct locks active, but we don't do that now.
    //第一次进来,所有缓存都找不到
    lockp->lock();

    {
        SyncData* p;
        SyncData* firstUnused = NULL;
        for (p = *listp; p != NULL; p = p->nextData) {//cache中已经找到
            if ( p->object == object ) {//如果不等于空,且与object相似
                result = p;//赋值
                // atomic because may collide with concurrent RELEASE
                OSAtomicIncrement32Barrier(&result->threadCount);//对threadCount进行++
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object 没有与当前对象关联的SyncData
        if ( (why == RELEASE) || (why == CHECK) )
            goto done;
    
        // an unused one was found, use it 第一次进来,没有找到
        if ( firstUnused != NULL ) {
            result = firstUnused;
            result->object = (objc_object *)object;
            result->threadCount = 1;
            goto done;
        }
    }

    // Allocate a new SyncData and add to list.
    // XXX allocating memory with a global lock held is bad practice,
    // might be worth releasing the lock, allocating, and searching again.
    // But since we never free these guys we won't be stuck in allocation very often.
    posix_memalign((void **)&result, alignof(SyncData), sizeof(SyncData));//创建赋值
    result->object = (objc_object *)object;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe_lock);
    result->nextData = *listp;
    *listp = result;
    
 done:
    lockp->unlock();
    if (result) {
        // Only new ACQUIRE should get here.
        // All RELEASE and CHECK and recursive ACQUIRE are 
        // handled by the per-thread caches above.
        if (why == RELEASE) {
            // Probably some thread is incorrectly exiting 
            // while the object is held by another thread.
            return nil;
        }
        if (why != ACQUIRE) _objc_fatal("id2data is buggy");
        if (result->object != object) _objc_fatal("id2data is buggy");

#if SUPPORT_DIRECT_THREAD_KEYS
        if (!fastCacheOccupied) { //判断是否支持栈存缓存,支持则通过KVC形式赋值 存入tls
            // Save in fast thread cache
            tls_set_direct(SYNC_DATA_DIRECT_KEY, result);
            tls_set_direct(SYNC_COUNT_DIRECT_KEY, (void*)1);//lockCount = 1
        } else 
#endif
        {
            // Save in thread cache 缓存中存一份
            if (!cache) cache = fetch_cache(YES);//第一次存储时,对线程进行了绑定
            cache->list[cache->used].data = result;
            cache->list[cache->used].lockCount = 1;
            cache->used++;
        }
    }

    return result;
}

第一步:首先在 tls 即线程缓存中查找。
a.在 tsl_get_direct方法中以线程为key,通过 kvc的方式获取与之绑定的 SyncData,即线程 data,其中的tls(),表示本地局部的线程缓存;
b.判断获取的data是否存在,以及判断data 中是否能找到对应的 object;
c.如果都找到了,在tsl_get_direct方法中以 KVC 的方式获取 lockCount,用来记录对象被锁了几次(即锁的嵌套次数)
d.如果data中的threadCount 小于等于0,或者lockCount 小于等于0时,则直接崩溃
e.通过传入的why,判断操作类型:
e.1 如果是ACQUIRE,表示加锁,则进行lockCount++,并保存到tls缓存
e.2 如果是RELEASE,表示释放,则进行lockCount--,并保存到tls缓存。如果lockCount 等于 0,从tls中移除线程data
e.3 如果是CHECK,则什么也不做

第二步:如果 tls中没有,则在cache缓存中查找
a.通过 fetch_cache方法查找 cache 缓存中是否有线程
b.如果有,则遍历 cache 总表,读取线程对应的 SyncCacheItem
c. 从SyncCacheItem中取出data,然后后续步骤与tls的匹配是一致的

第三步:如果 cache 中也没有,即第一次进来,则创建 SyncData,并存储到相应缓存中:
a.如果在cache中找到线程,且与object相等,则进行赋值、以及threadCount++
b.如果在cache中没有找到,则threadCount等于1

针对tlscache 缓存,底层的表结构如下:

image.jpeg

  • 哈希表结构中通过SyncList结构来组装多线程的情况
  • SyncData通过链表的形式组装当前可重入的情况
  • 下层通过tls线程缓存、cache缓存来进行处理
  • 底层主要有两个东西:lockCountthreadCount,解决了递归互斥锁,解决了嵌套可重入
3.11 总结

我们对iOS的各种线程同步方案体验了一下:

  • OSSpinLock由于其不休眠特性,所以它的效率是非常高的,但是由于安全问题,苹果建议我们使用os_unfair_lock取而代之,并且效率还要高于前者。
  • pthread_mutex是一种跨平台的解决方案,性能也不错。当然还有苹果的GCD解决方案,也是挺不错的。
  • 对于NS开头的那些OC下的解决方案,虽然本质也还是基于pthread_mutex的封装,但是由于多了一些面向对象的操作开销,效率不免要下降。
  • 性能最差的是@synchronized方案,虽然它的使用是最简单的,但因为它的底层封装了过于复杂的数据结构,导致了性能低下。
    使用推荐如下:
  • os_unfair_lock(推荐🌟🌟🌟🌟🌟)
  • OSSpinLock(不安全⚠️⚠️)
  • dispatch_semaphore(推荐🌟🌟🌟🌟🌟)
  • pthread_mutex(推荐🌟🌟🌟🌟)
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)(推荐🌟🌟🌟)
  • NSLock(🌟🌟🌟)
  • NSCondition(🌟🌟🌟)
  • pthread_mutex(recursive)(🌟🌟)
  • NSRecursiveLock(🌟🌟)
  • NSConditionLock(🌟🌟)
  • @synchronized(最不推荐)

4. 自旋锁和互斥锁的对比

4.1 什么情况下选择自旋锁更好?

自旋锁的特点:效率高、安全性不足、占用CPU资源大,因此选择自旋锁依据原则如下:

  • 预计线程等待锁的时间很短
  • 加锁的代码(临界区)经常被调用,但是竞争的情况发生概率很小,对安全性要求不高
  • CPU资源不紧张
  • 多核处理器
4.2 什么情况下选择互斥锁更好?

互斥锁特点:安全性突出、占用CPU资源小,休眠/唤醒过程要消耗CPU资源,因此选择互斥锁依据原则如下:

  • 预计线程等待锁的时间比较长
  • 单核处理器
  • 临界区有IO操作
  • 临界区代码复杂或者循环量大
  • 临界区的竞争非常激烈,对安全性要求高
4.3 为什么iOS中几乎不用atomic

atomic是用于保证属性的setter、getter方法的原子性操作的,本质就是在getter和setter内部增加线程同步的锁,用的锁实质上也是os_unfair_lock
但是atomic只是保证读写操作的线程同步,对于可变数组、可变字典来说,如果你使用atomic修饰,在添加元素的时候,并不是线程安全的:

NSMutableArray *arr = self.dataArr;//getter方法是安全的
for (int i = 0; i<5; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [arr addObject:@"1"];//这里会有多线程操作_dataArr,atomic无法保证这里的线程同步
    });
}

所以说atomic并不能完全保证多线程安全问题。
我们几乎不会用到atomic,因为propertyiOS代码中调用的太频繁了,会导致锁的过度使用,消耗CPU资源,所以我们只需要针对具体会出现多线程隐患的地方加锁就行了,需要加锁的时候再去加。

4.4 多线程读写安全

在上面存取钱的示例中,存、和取其实就是对共享资源的读和写,假如我们有如下两个操作分别只包含读操作和写操作:

- (void)read {
    sleep(1);
    NSLog(@"read");
}

- (void)write
{
    sleep(1);
    NSLog(@"write");
}

其实读操作的目的,只是取出数据,并不会修改数据,所以多线程同时进行读操作是没问题的,不需要考虑线程同步的问题,写操作是导致多线程安全问题的根本因素。所以为了读写安全,解决方案其实就是多读单写:

  • 要求1: 同一时间,只能有1个线程进行写的操作
  • 要求2: 同一时间,允许有多个线程进行读的操作
  • 要求3: 同一时间,不允许既读又写,就是说读操作和写操作之间是互斥关系

iOS有两种方案可以实现上述的读写安全需求

  1. pthread_rwlock:读写锁
  2. dispatch_barrier_async:异步栅栏调用
  • pthread_rwlock
    使用pthread_rwlock,等待锁的线程会进入休眠,API如下:
//初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
//读操作加锁
pthread_rwlock_rdlock(&lock);
//读操作尝试加锁
pthread_rwlock_tryrdlock(&lock);
//写操作加锁
pthread_rwlock_wrlock(&lock);
//写操作尝试加锁
pthread_rwlock_trywrlock(&lock);
//解锁
pthread_rwlock_unlock(&lock);
//销毁锁
pthread_rwlock_destroy(&lock);
  • dispatch_barrier_async
    使用dispatch_barrier_async有一个注意点,这个函数接受的并发队列参数必须是你自己手动创建的(dispatch_queue_create),如果接受的是一个串行队列或者是一个全局并发队列,那么这个函数的效果等同于dispatch_async函数。
    具体介绍,可以看另外一篇文章,里面有对于GCD常用方法的介绍:
    //www.greatytc.com/p/3f9910293401
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345