多线程六——加锁方案一 : OSSpinLock

一、iOS 中的线程同步方案 -> 加锁

  • OSSpinLock:自旋锁
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL)
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

二、OSSpinLock

  1. 含义
  • OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
  • 目前已经不再安全,可能会出现优先级反转问题
    • 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
    • 需要导入头文件#import <libkern/OSAtomic.h>
  1. 主要代码
    在这里插入图片描述

三、 示例程序: 卖票

  • 希望关键程序加锁

没加锁代码:

/// 卖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);
    }
}

八、锁的多样性创建

  1. 只在一个方法中用到的锁,可以不创建 属性,使用 static 即可
- (void)saleTicket {
    static OSSpinLock ticketLock = OS_SPINLOCK_INIT;
    OSSpinLockLock(&ticketLock);
    [super saleTicket];
    OSSpinLockUnlock(&ticketLock);
}
  1. 如果是 两个以上的方法共用一把锁,可以写成 属性,或者是 外界 的 static 全局变量。
  2. 赋值的时候,可以在 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_);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值