1、OSSpinLock(自旋锁)

1.1、OSSpinLock简介

1.2.1、自旋

OSSpinLock叫做“自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源,类似于加锁后,等待的线程会执行一个while循环:

  1. while(已加锁);

当解锁后再继续执行任务。

1.2.2、可能会出现优先级反转问题

目前已经不再安全(iOS10之后已经弃用),可能会出现优先级反转问题,如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
原因是系统会使用时间片轮转调度算法来调度线程,给不同的线程分配时间来执行各自的任务,优先级高的线程分配较多的时间,优先级小的线程分配较少的时间。
比如有两个线程thread1和thread2,thread1的优先级比较高,当thread2给任务加锁后,正在执行的过程中,thread1执行此任务,发现已经加锁,开始“自旋”,而由于thread1的优先级比较高,系统会分配更多的时间给thread1,就会导致thread2没时间完成任务,也就无法解锁,导致代码无法继续执行。

1.2、OSSpinLock使用

使用时需要导入头文件:

  1. #import <libkern/OSAtomic.h>

有如下API:

  1. - (void)spinLockAPI {
  2. // 初始化
  3. OSSpinLock lock = OS_SPINLOCK_INIT;
  4. // 尝试加锁
  5. OSSpinLockTry(&lock);
  6. // 加锁
  7. OSSpinLockLock(&lock);
  8. // 解锁
  9. OSSpinLockUnlock(&lock);
  10. }

卖票问题使用OSSpinLock代码如下:

  1. - (void)saleTicket {
  2. // 加锁
  3. OSSpinLockLock(&_ticketLock);
  4. int oldTicketsCount = self.ticketsCount;
  5. sleep(.2);
  6. oldTicketsCount--;
  7. self.ticketsCount = oldTicketsCount;
  8. NSLog(@"剩余%d张票", self.ticketsCount);
  9. // 解锁
  10. OSSpinLockUnlock(&_ticketLock);
  11. }

在任务开始时执行加锁方法,在任务结束时执行解锁方法,当其他线程执行任务时,发现_ticketLock已经加锁,就会开始等待,等解锁后再加锁并执行任务,这样就保证了线程的同步。

2、os_unfair_lock

1、os_unfair_lock简介

os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持。从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等。使用时需要导入头文件:

  1. #import <os/lock.h>

有如下API:

  1. - (void)unfairLockAPI {
  2. // 初始化
  3. os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
  4. // 尝试加锁
  5. BOOL result = os_unfair_lock_trylock(&lock);
  6. // 加锁
  7. os_unfair_lock_lock(&lock);
  8. // 解锁
  9. os_unfair_lock_unlock(&lock);
  10. }

使用方式和OSSpinLock相同:

  1. - (void)__saleTicket {
  2. os_unfair_lock_lock(&_ticketLock);
  3. [super __saleTicket];
  4. os_unfair_lock_unlock(&_ticketLock);
  5. }

2、死锁问题

加锁后需要解锁,如果不解锁就会造成任务一直不能再次执行,造成死锁:

  1. - (void)__drawMoney {
  2. os_unfair_lock_lock(&_moneyLock);
  3. [super __drawMoney];
  4. //没有解锁
  5. }

3、自旋锁原理探究

添加多个线程访问一个加锁任务:

  1. - (void)ticketsTest {
  2. for (int i = 0; i < 10; i++) {
  3. [[[NSThread alloc] initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
  4. }
  5. }
  6. - (void)__saleTicket {
  7. OSSpinLockLock(&_ticketLock);
  8. [super __saleTicket];
  9. OSSpinLockUnlock(&_ticketLock);
  10. }

在OSSpinLockLock方法处添加断点,在第一条线程调用时,过掉断点,当第二条线程调用时,查看汇编代码(Debug - Debug workflow - Always show Disassembly)
在控制台输入si指令,逐条查看汇编代码运行过程:

  1. (lldb) si

先执行方法OSSpinLockLock方法:

  1. 0x10295a661 <+33>: callq 0x10295bf3e; symbol stub for: OSSpinLockLock

之后执行_OSSpinLockLockSlow方法:

  1. 0x7fff60335d4a <+11>: jne 0x7fff60336261; _OSSpinLockLockSlow

在内部会进行循环调用,每次执行+41行时都会跳转到+14行开始循环。

  1. 0x7fff6033626f <+14>: movl (%rdi), %eax
  2. ......
  3. ......
  4. ......
  5. 0x7fff6033628a <+41>: jmp 0x7fff6033626f; <+14>

所以自旋锁的加锁原理相当于执行一个while循环。

4、互斥锁原理探究

同样添加多个线程访问一个加锁任务:

  1. - (void)ticketsTest {
  2. for (int i = 0; i < 10; i++) {
  3. [[[NSThread alloc] initWithTarget:self selector:@selector(__saleTicket) object:nil] start];
  4. }
  5. }
  6. - (void)__saleTicket {
  7. os_unfair_lock_lock(&_ticketLock);
  8. [super __saleTicket];
  9. os_unfair_lock_lock(&_ticketLock);
  10. }

同样在os_unfair_lock_lock方法处添加断点,在第一条线程调用时,过掉断点,当第二条线程调用时,查看汇编代码,在控制台输入si指令,逐条查看汇编代码运行过程:

  1. (lldb) si

先执行os_unfair_lock_lock方法:

  1. 0x1018103d1 <+33>: callq 0x101810f9a; symbol stub for: os_unfair_lock_lock

之后执行_os_unfair_lock_lock_slow方法:

  1. 0x7fff603335fc <+19>: jmp 0x7fff60334023; _os_unfair_lock_lock_slow

之后执行__ulock_wait方法:

  1. 0x7fff603340c0 <+157>: callq 0x7fff603396c8; symbol stub for: __ulock_wait

最后执行syscall方法:

  1. 0x7fff603049ec <+8>: syscall

执行完syscall方法后,断点自动过掉了,是因为线程开始休眠了。
所以互斥锁的加锁原理是让线程休眠。
(pthread_mutex原理与os_unfair_lock相同,最后都会调用syscall方法)