一、iOS 中的线程同步方案 -> 加锁
- OSSpinLock:自旋锁
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- @synchronized
二、OSSpinLock
- 含义
- OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
- 目前已经不再安全,可能会出现优先级反转问题
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
- 需要导入头文件#import <libkern/OSAtomic.h>
- 主要代码
三、 示例程序: 卖票
- 希望关键程序加锁
没加锁代码:
/// 卖1张票
- (void)saleTicket {
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩 %d 张票 -%@", oldTicketsCount, [NSThread currentThread]);
}
/// 卖票演示
- (void)ticketTest {
self.ticketsCount = 15;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0 ; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0 ; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(queue, ^{
for (int i = 0 ; i < 3; i++) {
[self saleTicket];
}
});
}
加锁代码:
加锁: 别的线程无法再进行访问
解锁: 用完后,解除锁定,让其他线程可以访问。
如果使用须导入 #import <libkern/OSAtomic.h>
第一次加锁:在 saleTicket 方法中 ,一开始就加锁
/// 卖1张票
- (void)saleTicket {
// 加锁 - 初始化 - iOS10 已经弃用
OSSpinLock look = OS_SPINLOCK_INIT;
// 加锁, 因为要传的是指针,所以传入地址
OSSpinLockLock(&look);
// 原来代码
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩 %d 张票 -%@", oldTicketsCount, [NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&look);
}
执行结果:
- 可以看到,这样的加锁 ,跟没有加一样,票数还是错误的。
- 这是因为
OSSpinLock look = OS_SPINLOCK_INIT;
是局部变量,每次进入到saleTicket
这个方法的时候,都会重新创建。 - 修改为:
@property(nonatomic,assign) OSSpinLock look;
- (void)viewDidLoad {
[super viewDidLoad];
// 加锁 - 初始化 - iOS10 已经弃用
_look = OS_SPINLOCK_INIT;
[self ticketTest];
}
/// 卖1张票
- (void)saleTicket {
// 加锁, 因为要传的是指针,所以传入地址
OSSpinLockLock(&_look);
// 原来代码
int oldTicketsCount = self.ticketsCount;
sleep(.2);
oldTicketsCount--;
self.ticketsCount = oldTicketsCount;
NSLog(@"还剩 %d 张票 -%@", oldTicketsCount, [NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_look);
}
打印结果:
- 打印结果可以看到:剩余票数正常
四、示例程序:存钱取钱
*没有加锁的 写法
- (void)viewDidLoad {
[super viewDidLoad];
// 加锁 - 初始化 - iOS10 已经弃用
_look = OS_SPINLOCK_INIT;
[self moneyTest];
}
/// 存钱,取钱演示
- (void)moneyTest {
self.money = 50;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0 ; i < 5; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0 ; i < 5; i++) {
[self drawMoney];
}
});
}
/// 取钱
- (void)drawMoney {
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取 20 , 还剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
}
/// 存钱
- (void)saveMoney {
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存 50 , 还剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
}
执行结果:没有对的
- 应该如何加锁?
- 是 存钱一把锁,取钱一把锁
- 还是 他们两个共用一把锁
思考:
* 存钱取钱 是否能同时执行?
* 不能
* 这两个操作在同一时间段只能执行一个 。
* 所以可以使用一把锁
* 如果 锁不一样,意味着 同一时间段可以两个一起执行。
* 只有大家共用一把锁,才能保证同一时间段只能执行一个操作。
加锁之后的 代码
- (void)viewDidLoad {
[super viewDidLoad];
// 加锁 - 初始化 - iOS10 已经弃用
_look = OS_SPINLOCK_INIT;
[self moneyTest];
}
/// 存钱,取钱演示
- (void)moneyTest {
self.money = 50;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
for (int i = 0 ; i < 5; i++) {
[self saveMoney];
}
});
dispatch_async(queue, ^{
for (int i = 0 ; i < 5; i++) {
[self drawMoney];
}
});
}
/// 取钱
- (void)drawMoney {
OSSpinLockLock(&_look);
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取 20 , 还剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_look);
}
/// 存钱
- (void)saveMoney {
OSSpinLockLock(&_look);
int oldMoney = self.money;
sleep(.2);
oldMoney += 50;
self.money = oldMoney;
NSLog(@"存 50 , 还剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_look);
}
五、存钱取钱 和 卖票
如果存钱取钱 和 卖票 使用同一把锁,会出现什么事情?
- 意味着,同一时间段,只能执行一个操作,要不存钱,要不取钱,要不 卖票
- 但没有必要,效率会降低
- 因为 存钱取钱 和 卖票 不是一个事情,访问的变量不一样。卖票访问的是 ticketsCount 变量。 存钱取钱 访问的是 money 变量。两个互不相干。
- 只有当多个线程访问同一个变量(资源)时,才需要使用同一把锁。
六、OSSpinLock 解释
加锁其实就是 线程阻塞
常见的线程阻塞有两种
- 一种: 相当于写了一个 while 循环,一直在循环代码
- 另一种: 让线程直接睡眠
OSSpinLock 这个锁的线程阻塞就是 忙等。
- OSSpinLock 叫做”自旋锁”;
- 等待别人把锁解开的时间中,一直占用着CPU 资源。这种状态我们叫做 忙等。
- 一边忙着做事情,一边等待着 解开锁。
- 其实就相当于写了一个while 循环。
- 相当于写了这样一行代码
while(锁还没被放开);
- 每次执行的时候,都会看下锁是否被放开,如果没有被放开,就执行一次。如果被放开了,就不在执行。
目前 OSSpinLock 已经不在安全,可能会出现优先级翻转问题。
例如: 我们有三个线程 , thread1,thread2,thread3。当我们支持多线程,并开启三条线程时,有可能3条线程同时做事情。
系统是如何调度三条线程的?
- 安排时间给 三条线程,每条线程执行都执行一小会(也许三秒钟?)。
- 例如,先给 thread1 3秒钟,在给 thread2 3秒钟,在给 thread3 3秒钟。然后在给 thread1 3秒钟 … … 一直这样,直到3个线程执行完毕。
- 其实这是多线程的原理
- 上面的也可以叫做 时间片轮转调度算法
- 时间片轮转调度算法:操作系统在调度进程和线程时基本上都是按照上面的套路去做的。
- 它会牵扯到一个线程优先级问题
- 如果线程优先级 高,给分配的时间就多
使用自旋锁 会发生什么事情?
- 优先级翻转的问题
- thread1: 优先级高
- thread2: 优先级低
- 当 thread2 先进入 saleTicket 的锁中,就会先把
OSSpinLockLock(&_lock)
加锁。这样当线程1 进入时,发现所已经被加了。线程1 只能忙等。也就是在做while(未解锁)
这一行代码。 - 由于 thread1 的优先级高,很有可能 CPU 一直分配时间给 它,也就是说,CPU一直在做
while(未解锁)
这一行代码。 - 也就是说,CPU 可能没有时间在 分配给 thread2 ,会照成 thread2 的线程不能再往下执行,也就永远无法放开这把锁。
这就是 自旋锁会发生的事情。
如果是 睡眠,就不会发生这种事情。
线程就不分配时间给thread1, thread2 就可以继续往下走,就可以完成解锁操作。然后 thread1 从休眠中唤醒,发现锁已经解开,thread1 就进行加锁,往下执行。
这就是为啥在 使用 OSSpinLockLock 的时候,苹果会提示 这个锁已经 不在使用。
七、另一种加锁方式
OSSpinLockTry : 尝试加锁
* 如果内容没有被加锁,就尝试加锁
* 如果被加锁,就不执行大括号中的代码。
/// 取钱
- (void)drawMoney {
// 尝试加锁,返回BOOL 值
bool result = OSSpinLockTry(&_look);
// 如果没有被加锁,就尝试加锁。
// 如果被加锁,就不执行 大扩号中的代码,不会阻塞线程,代码继续往下走
if (result) {
int oldMoney = self.money;
sleep(.2);
oldMoney -= 20;
self.money = oldMoney;
NSLog(@"取 20 , 还剩 %d 元 - %@",oldMoney, [NSThread currentThread]);
// 解锁
OSSpinLockUnlock(&_look);
}
}
八、锁的多样性创建
- 只在一个方法中用到的锁,可以不创建 属性,使用 static 即可
- (void)saleTicket {
static OSSpinLock ticketLock = OS_SPINLOCK_INIT;
OSSpinLockLock(&ticketLock);
[super saleTicket];
OSSpinLockUnlock(&ticketLock);
}
- 如果是 两个以上的方法共用一把锁,可以写成 属性,或者是 外界 的 static 全局变量。
- 赋值的时候,可以在 initialize 方法中,用 单例初始化。
@implementation OSSpinLockDemo2
static OSSpinLock moneyLock_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
moneyLock_ = 0;
});
}
- (void)__drawMoney {
OSSpinLockLock(&moneyLock_);
[super __drawMoney];
OSSpinLockUnlock(&moneyLock_);
}
- (void)__saveMoney {
OSSpinLockLock(&moneyLock_);
[super __saveMoney];
OSSpinLockUnlock(&moneyLock_);
}