Files
knowledge-kit/Chapter1 - iOS/1.39.md
2026-01-02 10:28:57 +08:00

98 KiB
Raw Blame History

多线程探究

平时我们经常使用 GCD、锁、队列、block那这些概念和本质到底是什么

线程安全如何实现?

自旋锁、互斥锁区别是什么?

什么是死锁?

如果不清楚这些问题,带着问题,跟随本文来一探究竟

一、多线程方案

技术方案 简介 语言 线程生命周期 使用频率
pthread -一套通用的多线程API
适用于Unix\Linux\Windows等系统
跨平台\可移植
使用难度大
C 开发者手动管理 很少,底层监控会用到
NSThread 使用更加面向对象
简单易用,可直接操作线程对象
OC 开发者手动管理 偶尔
GCD 旨在替代NSThread等线程技术
充分利用设备的多核
C 系统自动管理 经常
NSOperation 基于GCD底层是GCD
比GCD多了一些更简单实用的功能
使用更加面向对象
OC 系统自动管理 经常
并发队列 自定义串行队列 主队列(串行)
同步sync 不开新线程、串行执行任务 不开新线程、串行执行任务 不开新线程、串行执行任务
异步async 开新线程、并发执行任务 开新线程、串行执行任务 不开新线程、串行执行任务

二、多线程死锁

什么是死锁?

队列任务引起的循环等待。

看几个 Demo 观察下死锁情况

Demo0

// 死锁
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_sync(dispatch_get_main_queue(), ^{  // Crash. Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
        NSLog(@"task");
    });
}

// 不死锁
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"task");
    });
    NSLog(@"2");
}
// console
1
task 
2
  
// 死锁
- (void)viewDidLoad {
  [super viewDidLoad];
  NSLog(@"1");
      dispatch_queue_t serialQueue = dispatch_queue_create("com.gcd.test", DISPATCH_QUEUE_SERIAL);
      dispatch_sync(serialQueue, ^{
          NSLog(@"task");
          dispatch_sync(serialQueue, ^{	// Crash.Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
              NSLog(@"3");
          });
      });
      NSLog(@"2");
}
// console
1
task

为什么一个死锁了,一个没有死锁?

第一个写法:是因为 viewDidLoad 默认是主队列上跑的,主队列也只有一个主线程。所以 viewDidLoadNSLog(@"task") 这2个任务都被放到主队列上等待被调度然而主线程在执行的时候 viewDidLoad 的执行依赖 NSLog(@"task") NSLog 的执行依赖 viewDidLoad,所以主队列任务循环等待,引起了死锁

第二个写法创建了一个新的串行队列队列里只加了1个任务 NSLog(@"task"),主队列中有 viewDidLoad,没有与之相互等待的任务,故而不会产生死锁。

第三个写法:创建了一个新的串行队列。主队列里只有 viewDidLoad 任务。在主线程执行获取任务的时候,先从主队列获取了 viewDidLoad 任务,然后从创建的串行队列中获取任务,先获取了 NSLog(@"task") 任务,由于同步执行,后面的代码执行需要等待当前任务的执行结束,但当前任务的执行结束又依赖后面的任务 NSLog(@"3")

Demo1

NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
    NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// 死锁

分析主队列是一个串行队列任务3等待 dispatch_sync 内的任务执行完毕,可 dispatch_sync 内的任务等待任务3执行互相等待产生死锁

Demo2

NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
    NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// 1 3 2 

Demo3

NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
    NSLog(@"执行任务2")
    dispatch_sync(queue, ^{ // 1
        NSLog(@"执行任务3");
    });
    NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
// 1 5 2 Crash

分析任务4等待 dispatch_sync 内的任务3dispatch_sync 内的任务3等待任务4执行互相等待

Demo4

NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
    NSLog(@"执行任务2");
    dispatch_sync(queue2, ^{ // 1
        NSLog(@"执行任务3");
    }); 
    NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
// 1 5 2 3 4 

分析不会死锁。因为在存在2个任务队列。所以会按照顺序各自从队列上取任务执行。

Demo5

NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0
    NSLog(@"执行任务2");
    dispatch_sync(queue, ^{ // 1
        NSLog(@"执行任务3");
    }); 
    NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
// 1 5 2 3 4

分析:为什么不会死锁?

  • 先打印1
  • 然后给并发队列派发了异步任务所以不会阻塞开启了子线程在子线程中打印了5
  • 并发队列里存在任务2然后先打印2
  • 然后用同步的方式给并发队列里添加了任务3同时里面还存在任务4
  • 是不是产生了一种假设任务4要执行必须等前面的任务3执行完毕任务3的执行也必须等任务4执行完毕造成互相等待死锁
  • 但别忘记这是并发队列,并发队列里的不同 task 可以同时执行,并不会互相等待。上一行的分析适用于串行队列。

总结:

  • 队列决定了任务执行完是否需要等待。任务决定是否可以产生新线程
  • 使用 sync 函数给当前串行队列派发同步任务,则会卡住当前串行队列,产生死锁

Demo6

- (void)test{
    NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    dispatch_queue_t  queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:3];
        NSLog(@"3");
    });
}
// 1 3

分析为什么打印1、3没有打印2。因为 -(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay; 底层是开启了定时器,定时器运行需要添加到 RunLoop。上述代码是在全局并发队列上开启子线程子线程中没有 RunLoop所以定时器没有运行。

Demo7

NSLog(@"1");
dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"2");
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"3");
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"4");
        });
    });
});
NSLog(@"5");
// console
1 2 3 4 5

只要是同步提交任务 dispatch_sync() 不管是提交到串行队列还是并发队列,都是在当前线程执行。所以都是在主线程中执行。

因为是并发队列,所以可以同时执行,不需要互相等待,则先提交 NSLog(@"2") 任务到全局并发队列中,然后执行。由于不需要等待,可以执行后面的代码,继续提交 NSLog(@"3") 到全局并发队列,继续执行,因为不需要等待,继续执行后面的代码。继续提交 NSLog(@"4") 到到全局并发队列,继续执行。之后从主队列取出 NSLog(@"5") 任务

Demo8

dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);

dispatch_sync(queue, ^{
  	NSLog(@"1");
});
dispatch_async(queue, ^{
  NSLog(@"2");
  dispatch_sync(queue, ^{
    // 这里有没有具体的逻辑都不影响,本质是一个任务 block区别是空 block 和非空 block。
  });
  NSLog(@"4");
});
dispatch_sync(queue, ^{
  	NSLog(@"5");
});
// console
1
2
死锁

Demo9

dispatch_sync(dispathc_get_main_queue(), ^{
  	NSLog(@"主队列同步");
});
// 死锁
dispatch_sync(dispathc_get_main_queue(), ^{});
// 死锁
dispatch_sync(dispatch_get_main_queue(), nil);
// 死锁

死锁总结

在当前串行队列上,使用 sync 函数给当前串行队列派发同步任务,则会卡住当前串行队列,产生死锁

当前队列是串行队列 A且以同步的形势派发了一个任务到同一个串行队列 A 上去)。

三、performSelector...withObject 底层原理剖析

performSelector...withObject

- (void)test{
    NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    dispatch_queue_t  queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil];
        NSLog(@"3");
    });
}
// 1 3 2

分析为什么现在又执行打印2了因为 -(id)performSelector:(SEL)aSelector withObject:(id)object; 是 Runtime API本质上就是 objc_msgSend,所以不需要 RunLoop 便可运行

查看 objc4 NSObject.m 即可

+ (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)((id)self, sel, obj);
}

performSelector...withObject...afterDelay 剖析以及经典问题

Demo1

QA为什么先打印1、3再打印2

  • 该方法会将 showLog 方法的调用封装成一个 NSTimer 定时器事件,并添加到当前线程的 RunLoop 中
  • 不会阻塞当前线程:调用后立即返回,继续执行后续代码,打印 2
  • NSTimer 的执行依赖 RunLoop 的运行:定时器事件需要 RunLoop 处于运行状态才能触发。由于主线程的 RunLoop 默认是开启的,因此无需手动启动
  • 即使延迟时间为 0任务也不会立即执行而是等待当前代码执行完毕RunLoop 进入下一次循环时才触发

可以理解为本轮 RunLoop 在唤醒状态下优先处理屏幕点击事件包括打印1、3同时内部给 RunLoop 提交了一个 NSTimer提交的 NSTimer 等本轮结束后下次 RunLoop 唤醒才执行。

所以先打印1、再打印3最后打印2

Demo2:

QA为什么 test 里的2没有打印

查看源码分析,如何查看 -(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay; 源码。

  • 未开源,但是可以设置断点查看汇编分析;

  • Apple 的 XNU 是参考 GNUstep,它将 Cocoa 的 OC 库重新实现并开源。虽然不是官方源代码,但是具有研究参考价值

查看 GUNStep 源码

- (void) performSelector: (SEL)aSelector
          withObject: (id)argument
          afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop        *loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer    *item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
                         target: self
                       argument: argument
                          delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}

通过源码分析可以看到:

  • performSelector...withObject...afterDelay... 本质是开启一个定时器,并添加到 RunLoop但没有启动 RunLoop
  • 打印1、2是由于他们不需要 RunLoop 的配合
  • 点击事件里通过 GCD 开启一个子线程,子线程默认没有 RunLoop。所以定时器里的逻辑没办法执行

所以代码改下就可运行。

注意:可能有一部分人会这么在子线程中添加 RunLoop会存在3无法打印的问题。为什么

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 本质上让 RunLoop 在指定模式下运行,直到发生以下情况之一:

  • 有事件到达并被处理

  • 到达指定的超时时间(beforeDate参数)

  • 如果 beforeDate 设置为 [NSDate distantFuture]RunLoop会无限期等待事件不会主动超时

所以改法也很简单有3种

第一种:在子线程方法中,手动关闭子线程中的 RunLoop

第二种:不要用 [NSDate distantFuture],设置个0秒也可以改为 [NSDate dateWithTimeIntervalSinceNow:0]]

第三种:不要用 [NSDate distantFuture],设置个0秒也可以改为 [NSDate dateWithTimeIntervalSinceNow:0]]。另外不要加 Port直接在子线程中先获取一次 RunLoop 就好,因为 ``performSelector...withObject...afterDelay... 已经给当前的 RunLoop 添加了 NSTimer只是没有开启。 分析 RunLoop 源码分析后会发现,在子线程中获取一次 RunLoop会默认创建一个 RunLoop。

所以要研究 iOS 底层的同学,看看 GUNStep 代码吧,这是宝藏

pthread 线程原理

Demo1:

同理GCD 虽然开启了子线程,但是 Block 结束后线程也就结束了。所以线程任务中的1秒后的任务肯定也结束了。

Demo2:

可以看到 NSThread 里的 block 执行结束后thread 结束了。后面的 performSelector 想在线程里执行任务,就会 crash。

分析:

  • 屏幕点击事件里创建了一个 NSThread用 block 的形势添加了一个任务。
  • 调用 start立马执行。执行完 block 里面的代码后thread 内已经没有任务了,则 thread 立马销毁(注意:并不是在 131 行打断点发现 thread 内存还存在就没问题,因为此时 block 任务还没执行)。
  • 然后 performSelector 向已退出的线程提交任务发生 crash

查看源码分析:

GUN NSThread.m 文件

- (void) start
{
	// ...
  pthread_attr_t	attr;
	// ...
  pthread_attr_init(&attr);
  /* Create this thread detached, because we never use the return state from
   * threads.
   */
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  /* Set the stack size when the thread is created.  Unlike the old setrlimit
   * code, this actually works.
   */
  if (_stackSize > 0)
    {
      pthread_attr_setstacksize(&attr, _stackSize);
    }
  if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
    {
      DESTROY(self);
      [NSException raise: NSInternalInconsistencyException
                  format: @"Unable to detach thread (last error %@)",
                  [NSError _last]];
    }
}


/**
 * Trampoline function called to launch the thread
 */
static void *nsthreadLauncher(void *thread) {
  NSThread *t = (NSThread*)thread;
  setThreadForCurrentThread(t);

  /*
   * Let observers know a new thread is starting.
   */
  if (nc == nil) {
      nc = RETAIN([NSNotificationCenter defaultCenter]);
   }
  [nc postNotificationName: NSThreadDidStartNotification
		    object: t
		  userInfo: nil];

  [t _setName: [t name]];
  [t main];
  // 执行完毕后退出,销毁线程
  [NSThread exit];
  // Not reached
  return NULL;
}


- (void) main {
  if (_active == NO) {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on inactive thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  // 执行线程中的方法
  [_target performSelector: _selector withObject: _arg];
}

+ (void) exit{
  NSThread	*t;

  t = GSCurrentThread();
  if (t->_active == YES) {
      unregisterActiveThread(t);

      if (t == defaultThread || defaultThread == nil) {
        /* For the default thread, we exit the process.
         */
        exit(0);
		} else{
          pthread_exit(NULL);
		}
  }
}

分析:

  • 线程入口函数的执行流程

    • 线程启动后,执行 main 函数
    • performSelector: 执行用户定义的任务
    • 任务执行完毕后,标记线程状态为 finished
    • 线程入口函数返回,触发底层 pthread_exit 线程终止,操作系统回收线程资源

线程销毁的直接原因

  • POSIX 线程(pthread)的特性:当线程的入口函数返回时,线程会立即终止,内核自动回收其资源(栈、寄存器状态等)
  • NSThread 对象虽然可能未被立即释放,但底层线程已销毁,无法再执行任务

所以解决办法也是在线程的 block 里面加 RunLoop让它保活

四、GCD API - 队列组

  • 实现异步并发执行任务1、任务2

  • 等任务1、2都执行完毕再回到主线程执行任务3

dispatch_queue_t  queue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    for (NSInteger index = 0; index< 5; index++) {
        NSLog(@"Task1: %@ - index:%zd", [NSThread currentThread], index);
    }
});
dispatch_group_async(group, queue, ^{
    for (NSInteger index = 0; index< 5; index++) {
        NSLog(@"Task2: %@ - index:%zd", [NSThread currentThread], index);
    }
});

dispatch_group_notify(group, queue, ^{
    dispatch_async(dispatch_get_main_queue(), ^{
        for (NSInteger index = 0; index< 5; index++) {
            NSLog(@"Task3: %@ - index:%zd", [NSThread currentThread], index);
        }
    });
});
// 等价于 
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
      for (NSInteger index = 0; index< 5; index++) {
          NSLog(@"Task3: %@ - index:%zd", [NSThread currentThread], index);
      }
});

五、多线程安全问题(资源访问)- 加锁

经典 Demo

会输出什么?

打印结果电脑速度快的话会有很多次打印出5.慢的话打印出大于5的几次。

分析因为在循环内部是全局并发队列。多线程的情况下执行异步任务任务的先后顺序没办法保证。可能线程1拿到a=0然后内部加了1.线程2一开始拿到a=0但是代码还没执行到a++在线程1里面a就已经变为2因为是 __block 修饰的。所以线程2里面拿到的a变成了a然后内部a++后a就是3.其他线程执行情况类似。

NSLog 属于 IO 流,比普通运算耗时。所以当能执行 NSLog 的时候a 一定是大于等于5的。某条线程 a 大于等于5之后就立马结束 while 循环,开始执行最后的 NSLog。

所以电脑越快打印5的次数更多。电脑慢的情况下可能会存在几次输出大于5的情况。

为什么需要锁?

多线程存在资源共享问题。比如多个线程对同一块内存,同时读或者写,导致不一致,很容易引发数据错乱和数据安全问题。典型的生产者消费者问题。比如多个线程访问同一个对象、同一个变量、同一个文件。计算机中看上去一个很简单的操作,背后往往是多个指令的操作,所以很容易发生多线程资源访问的问题。

比如,self.ticketCount++ 看似是原子操作,实际在底层会分解为多个步骤,涉及读取、计算和写入操作

拆解为:

// 1. 读取当前值到寄存器
int current = [self ticketCount]; 

// 2. 执行自增计算
int newValue = current + 1;

// 3. 将新值写回内存
[self setTicketCount:newValue];

X86 汇编为

; 读取 ticketCount 到 eax 寄存器
mov eax, [self.ticketCount] 

; 自增 eax 寄存器
inc eax 

; 将 eax 写回 ticketCount
mov [self.ticketCount], eax 

解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)

常见的线程同步技术是:加锁。

iOS 锁种类

常见的锁有:

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

OSSpinLock

使用

OSSpinLock 叫做”自旋锁”。缺点是:自旋类似于一个 while 循环,在死等。

使用的时候需要导入 #import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT 初始化

OSSpinLockLock(&lock); 加锁

OSSpinLockUnlock(&lock); 解锁

bool res = OSSpinLockTry(&lock) 尝试加锁(如果需要等待就不加锁直接返回 false如果不需等待则加锁返回 true

Demo:

@interface ViewController ()
@property (assign, nonatomic) OSSpinLock bankLock;
@property (nonatomic, assign) NSInteger money;
@end

@implementation ViewController
- (void)viewDidLoad{
    [super viewDidLoad];
    self.bankLock = OS_SPINLOCK_INIT;
    self.money = 100;
    [self moneyTest];
}
- (void)moneyTest {
    dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self saveMoney];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self withdrawMoney];
        }
    });
    // 100 + 10*50 - 10*10 = 500
}
- (void)saveMoney {
    OSSpinLockLock(&_bankLock);
    NSInteger previousMoney = self.money;
    sleep(0.2);
    previousMoney += 50;
    self.money = previousMoney;
    NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
    OSSpinLockUnlock(&_bankLock);
}
- (void)withdrawMoney {
    OSSpinLockLock(&_bankLock);
    NSInteger previousMoney = self.money;
    sleep(0.2);
    previousMoney -= 10;
    self.money = previousMoney;
    NSLog(@"取20还剩%zd元 - %@", self.money, [NSThread currentThread]);
    OSSpinLockUnlock(&_bankLock);
}
@end

注意:多线程加锁必须是同一把锁,也就是第一次创建锁的时候,应该保存起来,后续其他线程访问的时候,继续使用同一把锁,否则每次访问都创建锁,则多线程锁对资源的保护效果就达不到。

存在问题

  • 等待锁的线程会处于忙等busy-wait状态一直占用着 CPU 资源

  • 不安全,可能会出现优先级反转问题

  • 如果等待锁的线程优先级较高它会一直占用着CPU资源优先级低的线程就无法释放锁

优先级反转问题

线程本质上就是 CPU 高速切换,系统分配很少的时间段分别给不同的线程,导致用户看上去是同时在做多个线程内的事情。操作系统会使用基于优先级抢占式调度算法。高优先级的线程始终在低优先级线程前执行。

高优先级任务被低优先级任务阻塞导致高优先级任务迟迟得不到调度。但其他中等优先级的任务却能抢到CPU资源。从现象上来看好像是中优先级的任务比高优先级任务具有更高的优先权。

操作系统通常采用抢占式调度策略,规则如下:

  • 高优先级任务优先:只要高优先级任务处于就绪状态(未阻塞),它总能抢占低优先级任务的 CPU 时间。
  • 锁的阻塞行为:操作系统调度器的核心逻辑是:仅从就绪队列Ready Queue中选择任务执行。所以当任务因等待锁而阻塞时,它的优先级对调度不再产生影响,直到锁被释放

举个例子:假设存在三个任务,优先级为 H > M > L,且 L 持有某个锁:

  1. 初始状态:
    • L 持有锁,并在 CPU 上运行,因为此时没有更高优先级的任务需要执行
    • 过了一会儿H 请求锁,但锁已被 L 持有,因此 H 被阻塞,忙等
    • 再过了一会儿M 处于就绪状态,但不需要锁
  2. M 抢占 CPU
    • 出于时间片轮转算法,当 L 的时间片用完或被其他原因中断时,调度器会选择下一个最高优先级的就绪任务执行
    • 此时 H 因等待锁被阻塞即使优先级最高但出于等待锁的状态下H 的状态变为 Blocked,会被移出就绪队列。调度器不再将 H 视为候选任务M 的优先级高于 L因此 M 抢占 CPU 并开始执行
  3. L 无法释放锁:
    • M 的执行导致 L 无法继续运行,因此 L 无法完成工作并释放锁
    • H 继续被阻塞。所以产生高优先级的 H 一直在等待,中等优先级的 M 被执行的优先级反转现象

当高优先级任务正等待信号量(此信号量被一个低优先级任务拥有着)的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行类似死锁]的情形发生

为了解决优先级反转问题,可以采取以下策略:

  1. 优先级天花板策略Priority Ceiling当任务使用共享资源时将其优先级提高到访问该资源的所有任务的最高优先级或某个确定的优先级即“优先级天花板”。这样可以确保持有资源的任务不会被其他低优先级的任务抢占从而避免了优先级反转。
  2. 优先级继承策略Priority Inheritance当一个任务被阻塞并等待一个低优先级任务释放资源时将低优先级任务的优先级提升到等待它的最高优先级任务的优先级。这样可以确保低优先级任务能够尽快释放资源从而使高优先级任务能够继续执行。

上面的代码改进下

- (void)saveMoney {
    if (OSSpinLockLock(&_bankLock)) {
        NSInteger previousMoney = self.money;
        sleep(0.2);
        previousMoney += 50;
        self.money = previousMoney;
        NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
        OSSpinLockUnlock(&_bankLock);
    }
}
- (void)withdrawMoney {
    if (OSSpinLockLock(&_bankLock)) {
        NSInteger previousMoney = self.money;
        sleep(0.2);
        previousMoney -= 10;
        self.money = previousMoney;
        NSLog(@"取20还剩%zd元 - %@", self.money, [NSThread currentThread]);
        OSSpinLockUnlock(&_bankLock);
    }
}

汇编剖析实现原理

自旋锁是指在等锁的时候通过类似 while 循环的代码,让线程忙碌等到锁的到来。

自旋锁是一种特殊的锁机制,当线程试图获取锁但失败时,它会在一个循环中持续尝试(即“自旋”),而不是立即阻塞。这可以在某些情况下提高性能,尤其是当锁被持有的时间很短时。

为了调试方便开启10个线程去执行 saveMoney 方法为了查看自旋锁的等是什么实现。我们给里面休眠600s。同时 Xcode - Debug - DebugWorkflow - Always Show Disassembly

lldb 模式下调试汇编有几个指令

c 代表 continue

sistep instruction简写为 stepisi。当你在 Xcode 汇编面板看到某个认识或者可疑符号,断点在这一行的时候,在下方 lldb 面板,属于 si即可进入内部实现。

第一步:当第二次调用 saveMoney 方法,开启汇编调试

看到可疑方法 OSSpinLockLock给它加断点看到第10行高亮了。lldb 模式输入 c敲回车。次数输入 si 即可进入 OSSpinLockLock  方法内部调试

第二步:继续输入 si敲回车

第三步:看到可疑方法 _OSSpinLockLockSlow给它加断点lldb 输入 C。此时断点到这一行了继续输入 si。

第四步:在 OSSpinLockLockSlow 方法内部调试,不断输入 si。

发现不断 si 最终一直会在第6行到第19行之间执行。懂汇编的会发现这其实是一个 while 循环。便可以证明自旋锁 OSSpinLock 在等锁的时候,底层实现是执行 while 循环,忙等,“太浪费性能了”(如果使用锁资源的线程任务很简单,那自旋也是高效的,可以快速获取锁。)

结论OSSpinLock 底层就是一个自旋锁,内部不断循环,盲等。

思考

OSSpinLock 效率这么低,那使用场景是什么?

  • 短临界区与多核优化

    自旋锁的核心优势在于 避免线程上下文切换的开销。在以下场景中OSSpinLock 的性能可能优于传统互斥锁(如 pthread_mutex

    • 锁持有时间极短(如几纳秒到微秒级):忙等的 CPU 消耗低于线程休眠与唤醒的开销
    • 多核 CPU 环境:当线程在另一个核心上即将释放锁时,忙等线程可以立即获取锁,无需等待调度器介入
  • 实现简单且无系统调用

    • 用户态实现OSSpinLock 完全在用户空间运行无需陷入内核态减少了系统调用syscall的开销。
    • 低延迟:对于高频、轻量级的锁操作(如计数器自增),自旋锁的响应速度更快

虽然它有合适的使用场景,但 Apple 已经标记为废弃了,所以最好别用,否则某个版本出现什么不符合预期的行为,就有苦说不出了。

os_unfair_lock

使用

os_unfair_lock 用于取代不安全的 OSSpinLock 从iOS10开始才支持。使用的时候需要导入头文件 #import <os/lock.h>

从底层调用看,等待 os_unfair_lock 锁的线程会处于休眠状态,并非忙等(自旋锁会忙等)

初始化 os_unfair_lock moneylock = OS_UNFAIR_LOCK_INIT;

加锁 os_unfair_lock_lock(&_moneylock);

解锁 os_unfair_lock_unlock(&_moneylock);

尝试加锁 os_unfair_lock_trylock(&_moneylock)

继续对存取钱 Demo 用 os_unfair_lock 实现

@interface ViewController ()
@property (nonatomic, assign) NSInteger money;
@property (nonatomic, assign) os_unfair_lock moneylock;
@end

@implementation ViewController
- (void)viewDidLoad{
    [super viewDidLoad];
    self.moneylock = OS_UNFAIR_LOCK_INIT;
    self.money = 100;
    [self moneyTest];
}
- (void)moneyTest {
    dispatch_queue_t queue = dispatch_queue_create("com.lbp.money.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self saveMoney];
        }
    });
    dispatch_async(queue, ^{
        for (int i = 0; i < 10; i++) {
            [self withdrawMoney];
        }
    });
    // 100 + 10*50 - 10*10 = 500
}
int cursorr = 1;
- (void)saveMoney {
    NSLog(@"current cursor %d", cursorr);
    cursorr++;
    os_unfair_lock_lock(&_moneylock);
    NSInteger previousMoney = self.money;
    sleep(0.2);
    previousMoney += 50;
    self.money = previousMoney;
    NSLog(@"存50还剩%zd元 - %@", self.money, [NSThread currentThread]);
    os_unfair_lock_unlock(&_moneylock);
}
- (void)withdrawMoney {
    os_unfair_lock_lock(&_moneylock);
    NSInteger previousMoney = self.money;
    sleep(0.2);
    previousMoney -= 10;
    self.money = previousMoney;
    NSLog(@"取20还剩%zd元 - %@", self.money, [NSThread currentThread]);
    os_unfair_lock_unlock(&_moneylock);
}
@end

假如对存钱过程,忘记解锁怎么办?产生死锁,如下

添加 cursor 标记死锁是发生在 saveMoney 方法执行的第几次。发现是第二次。因为第一次锁没有任何使用方,所以加锁成功,当第二次加锁的时候发现锁没有释放,所以产生死锁。

这时候使用尝试加锁 API os_unfair_lock_trylock 即可成功如下

汇编剖析实现原理

同样方式看看 ,按照上述调试汇编代码的步骤,我将关键步骤截图如下

结论:可以看到 os_unfair_lock 在锁等待的时候,底层调用的是 sysCall,当这一步执行后会发现后续代码都不执行了,也就是调用系统底层能力,线程真正休眠了,而不是一个循环忙等的实现,所以性能好。

系统对其描述是:Low-level lock that allows waiters to block efficiently on contention.,即低级锁,低级锁的特点是等不到锁就休眠。

在并发编程中,设计一种低级别锁,能够使等待线程在竞争时高效阻塞(而非忙等),通常需要从用户态切换到内核态这样的协作机制

pthread_mutex

使用

mutex 叫做”互斥锁”,等待锁的线程会处于休眠状态。使用时需要引入 #import <pthread.h>

使用:

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(&_moneyLock, &attr);
// 释放属性内存
pthread_mutexattr_destroy(&attr);
// 加锁
pthread_mutex_lock(&_moneyLock);
// 解锁
pthread_mutex_unlock(&_moneyLock);
// 释放锁内存
pthread_mutex_destroy(&_moneyLock);

其中 pthread_mutexattr_settype(pthread_mutexattr_t *, int); 第二个参数有4个枚举值

/*
 * Mutex type attributes
 */
#define PTHREAD_MUTEX_NORMAL        0
#define PTHREAD_MUTEX_ERRORCHECK    1
#define PTHREAD_MUTEX_RECURSIVE        2
#define PTHREAD_MUTEX_DEFAULT        PTHREAD_MUTEX_NORMAL

如果类型选 PTHREAD_MUTEX_DEFAULT 或者 PTHREAD_MUTEX_NORMAL 则可以省略 pthread_mutexattr_t 的创建,直接传 NULLpthread_mutex_init(&_moneyLock, NULL)

使用如下

化身递归锁

如果在某个方法内部递归调用自身怎么实现,好像挺简单的,直接内部调用即可。

- (void)otherTest {
    pthread_mutex_lock(&_lock);
    static int count = 0;
    count++;
    NSLog(@"%s", __func__);
    if (count<10) {
        [self otherTest];
    }
    pthread_mutex_unlock(&_lock);
}

- (void)sayHi {
    NSLog(@"Hello");
}
// console
-[PThreadMutexRecursiveLockTester otherTest]

只打印了 1。为什么因为第一次调用正常加锁然后递归调用自身第二次调用的时候尝试加锁但是这时候第一次调用时候所占用的锁还没释放会发生死锁。

我们的实际编程中,存在递归函数的情况。上面学完的锁,都不能满足该情况。执行函数 test然后加锁然后继续调用 test要加锁发现锁被占用了则会死锁。所以引进了递归锁。

递归锁的工作流程:先加锁,然后递归调用,再继续加锁,再调用再加锁,最后一次函数执行完则解锁,出栈后继续解锁,再解锁。类似于 NodeJS 的洋葱模型,效果等价于

+ 代表加锁;- 代表解锁
线程1: otherTest in +
				otherTest in +
					otherTest in +
----------------------------------
					otherTest out-
				otherTest out-	
      otherTest out-					

巧妙的是:互斥锁 pthread_mutex_lock 提供实现该功能的 API。只需要在互斥锁初始化地方将属性修改为 PTHREAD_MUTEX_RECURSIVE。即 pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); 就可以实现递归锁的效果了。

即:在同一个线程中可以多次获取同一把锁。并且不会死锁

改进后的效果如下

QA互斥递归锁可以在不同线程中加锁吗

不可以线程1加锁后线程2尝试加锁的时候发现锁已经被其他线程所使用线程2则等待。

汇编剖析实现原理

输入 si 继续跟进可以看到还是在执行我们自己的代码LockExplore image 的 pthread_mutex_lock 方法

继续输入 si 跟进

可以看到此时调用到系统 libsystem_pthread.dylib 库的 pthread_mutex_lock 方法了。

第41行看到关键函数继续输入 si 进去看看

可以看到内部第62行关键函数调用了 _pthread_mutex_firstfit_lock_wait 方法。此时继续输入 si 跟踪看看

可以看到内部第25行调用了关键函数 __psynch_mutexwait,继续输入 si 看看

可以看到内部继续调用了系统 libsystem_pthread.dylib 库的 __psynch_mutexwait 方法。继续输入 si

可以看到内部第4行发生了系统调用 sysCall,执行完第四句指令,线程立马就结束了。

结论:汇编逐句研究了 pthread_mutex_t 会发现最后也是调用 syscall 做到线程休眠不像自旋锁一样OSSpinLock 在底层实现是 while 循环一样忙等,浪费资源。

使用注意事项

加解锁必须成对

加解锁必须成对出现,否则容易出现多线程性能问题。提供一种思路,利用 @try-finally 来保证加解锁必须成对存在,这个写法也是 Weex 官方的实现。 Weex 中的 WXThreadSafeMutableDictionary 提供了一个线程安全的字典,其本质是通过加 pthread_muext_t 锁来维护内部的一个字典的。 比如下面的代码

初始化锁相关的配置

@interface WXThreadSafeMutableDictionary ()
{
    NSMutableDictionary* _dict;
    pthread_mutex_t _safeThreadDictionaryMutex;
    pthread_mutexattr_t _safeThreadDictionaryMutexAttr;
}

@end

@implementation WXThreadSafeMutableDictionary

- (instancetype)initCommon
{
    self = [super init];
    if (self) {
        pthread_mutexattr_init(&(_safeThreadDictionaryMutexAttr));
        pthread_mutexattr_settype(&(_safeThreadDictionaryMutexAttr), PTHREAD_MUTEX_RECURSIVE); // must use recursive lock
        pthread_mutex_init(&(_safeThreadDictionaryMutex), &(_safeThreadDictionaryMutexAttr));
    }
    return self;
}

- (instancetype)init
{
    self = [self initCommon];
    if (self) {
        _dict = [NSMutableDictionary dictionary];
    }
    return self;
}

在字典操作的地方使用锁

- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey
{
    id originalObject = nil; // make sure that object is not released in lock
    @try {
        pthread_mutex_lock(&_safeThreadDictionaryMutex);
        originalObject = [_dict objectForKey:aKey];
        [_dict setObject:anObject forKey:aKey];
    }
    @finally {
        pthread_mutex_unlock(&_safeThreadDictionaryMutex);
    }
    originalObject = nil;
}

这么写的价值:解锁逻辑「绝对执行」,彻底避免死锁 这是 @try-finally 最核心的价值 ——无论 try 块内发生什么(正常执行、提前 return、抛异常finally 块的解锁逻辑一定会执行

对比无 try-finally 的写法

// Bad: 若setObject抛异常unlock不会执行→死锁
pthread_mutex_lock(&_mutex);
[_dict setObject:anObject forKey:aKey];
pthread_mutex_unlock(&_mutex); 

问题:[_dict setObject:anObject forKey:aKey] 可能抛异常(比如 aKey = nil 时会触发 NSInvalidArgumentException若没有 finally锁会被永久持有→其他线程调用 lock 时死锁,整个字典无法再操作。

设计优点:

  • @try-finallly:即使 try 内逻辑出错finally 也会执行 pthread_mutex_unlock保证锁最终释放这是线程安全的「兜底保障」
  • 注意,不是 try...catch...finally: 如果加了 catch 逻辑,则字典的 key 为 nil 产生的崩溃也会被捕获掉,这属于不符合预期的行为。因为 key 为 nil 产生的原因太多了,可能是业务代码异常,也可能是数据异常,也可能是逻辑错误,如果一刀切直接用 try...catch...finally 捕获了异常,但是没有配置异常的收集、上报、处理逻辑,属于边界不清晰,本质是为了解决加解锁不匹配而可能带来的线程安全问题,却"多管闲事",把字典 key 为 nil 本该向上跑的异常而卡住了(这个问题不再赘述,是一个经典的策略问题,端上的异常发生时,安全气垫的“做与不做”问题)
pthread_mutex_t 的加解锁函数返回值处理

「加解锁函数都有返回值,需要对返回值进行判断和处理」这个是意识也业务场景问题,先告诉你有返回值,看你的场景需要严格处理还是松散处理。类似 JS 的 use strict iOS 系统开源组件(如 libdispatch/GCD、Apple 官方开源代码以及知名第三方开源库AFNetworking、SDWebImage 等)都会严格检查 pthread 锁相关函数的返回值—— 因为系统 / 核心库需要保证鲁棒性,避免锁操作失败导致的死锁、崩溃或线程安全漏洞

  1. GCD 的处理libdispatch 核心文件dispatch/src/queue.c队列的线程安全实现
// libdispatch 中 pthread_mutex_lock 返回值检查的典型写法
static inline void _dispatch_mutex_lock(pthread_mutex_t *m) {
    int ret = pthread_mutex_lock(m);
    // 严格检查返回值仅允许「成功0」或「递归锁重复加锁EDEADLK针对递归锁场景
    if (ret != 0 && ret != EDEADLK) {
        // 系统级库会触发 crash 并打印错误,避免静默失败
        dispatch_fatal("pthread_mutex_lock failed: %d", ret);
    }
}

static inline void _dispatch_mutex_unlock(pthread_mutex_t *m) {
    int ret = pthread_mutex_unlock(m);
    if (ret != 0) {
        dispatch_fatal("pthread_mutex_unlock failed: %d", ret);
    }
}

说明:

  • 加锁
    • 对 pthread_mutex_lock除了「成功0仅允许递归锁的「重复加锁EDEADLK递归锁场景下 EDEADLK 是预期行为);
    • 非预期返回值直接触发 dispatch_fatal系统级崩溃避免锁异常导致的隐性问题
  • 解锁操作pthread_mutex_unlock仅接受「成功0失败则崩溃

思考:为什么 GCD 要这么做? 系统库是「基础设施」,锁操作失败(如 EINVAL/ENOMEM意味着系统资源耗尽或参数错误属于「致命错误」—— 与其静默运行导致更严重的线程安全问题,不如直接崩溃并暴露问题。

  1. AFNetworking网络库线程安全的缓存 / 队列实现) AFNetworking 的 AFURLSessionManager.m 中,对锁操作的返回值检查:
// AFNetworking 中锁操作的返回值检查
- (void)lock {
    int lockResult = pthread_mutex_lock(&_lock);
    NSAssert(lockResult == 0, @"Failed to lock mutex with error: %d", lockResult);
}

- (void)unlock {
    int unlockResult = pthread_mutex_unlock(&_lock);
    NSAssert(unlockResult == 0, @"Failed to unlock mutex with error: %d", unlockResult);
}

说明:

  • 用 NSAssert 检查返回值Debug 模式下断言失败会崩溃Release 模式下跳过);
  • 兼顾「调试阶段暴露问题」和「Release 阶段不影响运行」;
  • 符合第三方库的「友好调试 + 线上稳定性」平衡原则。
  1. CocoaLumberjack日志库线程安全的日志队列 CocoaLumberjack 的 DDLog.m 中,锁返回值检查的严谨写法:
- (void)initLock {
    pthread_mutexattr_t attr;
    int ret = pthread_mutexattr_init(&attr);
    if (ret != 0) {
        DDLogError(@"pthread_mutexattr_init failed: %d", ret);
        return;
    }
    
    ret = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    if (ret != 0) {
        DDLogError(@"pthread_mutexattr_settype failed: %d", ret);
        pthread_mutexattr_destroy(&attr);
        return;
    }
    
    ret = pthread_mutex_init(&_lock, &attr);
    if (ret != 0) {
        DDLogError(@"pthread_mutex_init failed: %d", ret);
        pthread_mutexattr_destroy(&attr);
        return;
    }
    
    pthread_mutexattr_destroy(&attr);
}

说明:

  • 「初始化→设置属性→创建锁」全链路检查返回值
  • 失败时「回滚操作」(如销毁已初始化的 attr避免资源泄漏
  • 打错误日志,便于问题排查

总结: 核心原则:锁操作的返回值检查不是 “可选的”,而是 “必须的”,区别仅在于「失败后是崩溃、打日志还是抛异常」,需根据组件的核心程度选择。

  • 系统级开源库(如 libdispatch严格检查返回值非预期失败直接崩溃保证系统稳定性
  • 第三方开源库AFNetworking/SDWebImage调试阶段断言 + Release 阶段日志(平衡调试和线上稳定性);
  • 业务组件(如你的字典):推荐「断言 + 日志」模式 ——Debug 暴露问题Release 不崩溃且留痕;

互斥锁的条件变量 pthread_cond_t

多线程环境下,很多时候没办法确保先有数据再消费,比如生产者-消费者问题,这时候就有互斥锁的另一个 API 了,即条件变量pthread_cond_t

使用

初始化互斥锁条件 pthread_cond_init(&_condition, NULL);

等待条件进入休眠,放开 mutex 锁,被唤醒后会再次对 mutex 加锁 pthread_cond_wait(&_condition, &_moneyLock);

激活一个等待该条件的线程 pthread_cond_signal(&_condition)

激活所有等待该条件的线程 pthread_cond_broadcast(&_condition)

可以看到同时调用 remove、add 方法

  • 执行 remove 方法先加锁,但是由于数组为空,这时候就不需要执行删除元素,然后执行 add 方法
  • add 方法要加锁,发现锁被 remove 方法占用了
  • remove 方法为了等有元素再去执行 remove 引入了互斥锁条件 pthread_cond_t,调用 pthread_cond_wait 。此时线程进入休眠,同时会释放锁。
  • add 方法内加完元素会调用 pthread_cond_signal 来激活等待该条件的线程,此时 remove 方法内的线程获得锁,此时再次加锁
  • remove 方法执行完线程任务后,再解锁。

NSLock、NSRecursiveLock

使用

NSLock 是对 mutex 普通锁pthread_mutex_t的封装

NSRecursiveLock 是对 mutex 递归锁pthread_mutex_t ,且 attr 为 PTHREAD_MUTEX_RECURSIVE的封装API 跟 NSLock 基本一致

区别在于一个是 c 语言版本的 API一个是 OC 版本的包装。

查看 GUN 源码可以看看到底是如何实现的

+ (void) initialize{
  static BOOL    beenHere = NO;
  if (beenHere == NO){
      beenHere = YES;
      /* Initialise attributes for the different types of mutex.
       * We do it once, since attributes can be shared between multiple
       * mutexes.
       * If we had a pthread_mutexattr_t instance for each mutex, we would
       * either have to store it as an ivar of our NSLock (or similar), or
       * we would potentially leak instances as we couldn't destroy them
       * when destroying the NSLock.  I don't know if any implementation
       * of pthreads actually allocates memory when you call the
       * pthread_mutexattr_init function, but they are allowed to do so
       * (and deallocate the memory in pthread_mutexattr_destroy).
       */
      pthread_mutexattr_init(&attr_normal);
      pthread_mutexattr_settype(&attr_normal, PTHREAD_MUTEX_NORMAL);
      pthread_mutexattr_init(&attr_reporting);
      pthread_mutexattr_settype(&attr_reporting, PTHREAD_MUTEX_ERRORCHECK);
      pthread_mutexattr_init(&attr_recursive);
      pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);

      /* To emulate OSX behavior, we need to be able both to detect deadlocks
       * (so we can log them), and also hang the thread when one occurs.
       * the simple way to do that is to set up a locked mutex we can
       * force a deadlock on.
       */
      pthread_mutex_init(&deadlock, &attr_normal);
      pthread_mutex_lock(&deadlock);

      baseConditionClass = [NSCondition class];
      baseConditionLockClass = [NSConditionLock class];
      baseLockClass = [NSLock class];
      baseRecursiveLockClass = [NSRecursiveLock class];

      tracedConditionClass = [GSTracedCondition class];
      tracedConditionLockClass = [GSTracedConditionLock class];
      tracedLockClass = [GSTracedLock class];
      tracedRecursiveLockClass = [GSTracedRecursiveLock class];

      untracedConditionClass = [GSUntracedCondition class];
      untracedConditionLockClass = [GSUntracedConditionLock class];
      untracedLockClass = [GSUntracedLock class];
      untracedRecursiveLockClass = [GSUntracedRecursiveLock class];
    }
}

可以看到 NSLock 底层就是 pthread_mutex_t。

再看看 NSRecursiveLock

@implementation NSRecursiveLock
- (id) init{
  if (nil != (self = [super init])) {
      if (0 != pthread_mutex_init(&_mutex, &attr_recursive)){
          DESTROY(self);
      }
  }
  return self;
}

底层就是 pthread_mutex_init。参数 attr_recursive 其实就是一个递归锁的属性。

pthread_mutexattr_init(&attr_recursive);
pthread_mutexattr_settype(&attr_recursive, PTHREAD_MUTEX_RECURSIVE);

NSRecursiveLock 不能在多线程下递归调用。@synchronized 可以在多线程下递归调用。底层原因是 TLS 有关。

Demo

NSLock 死锁

会发生死锁后续代码无法执行App 表现就是 ANR。重复对 NSLock 进行加锁可能导致死锁问题,同时也可能引发数据竞争和性能下降等并发相关隐患

针对上述的例子,可以用递归锁解决。可以重复加锁。

NSCondition

NSCondition 是对 pthread_mutex_t 和 `pthread_cond_t 的封装。

API

@interface NSCondition : NSObject <NSLocking>

- (void)wait NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (BOOL)waitUntilDate:(NSDate *)limit NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (void)signal NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (void)broadcast;

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

源码

- (id) init {
    if (nil != (self = [super init])) {
        if (0 != pthread_cond_init(&_condition, NULL)){
            DESTROY(self);
        } else if (0 != pthread_mutex_init(&_mutex, &attr_reporting)) {
            pthread_cond_destroy(&_condition);
            DESTROY(self);
        }
    }
    return self;
}

因为 NSCondtion 已经封装好锁和条件所以直接使即可。pthread_mutex_t 需要搭配 pthread_cond_t 一起使用。

Demo

观察本次打印顺序,可以看到:

  • 程序先执行 _remove 方法,先加锁,然后遇到 if 条件满足了,则执行 wait 。wait 干的事情是先解锁,然后等待另一个地方发送 signal
  • 然后 _add 方法得到了锁,加锁。开始执行 addObject 方法。然后立马调用 signal 方法
  • 可能看上去很快,感觉同一时刻在 _remove 方法中又得到了锁资源,然后删除了元素,最后释放了锁资源

疑问:调用 signal 方法后,另一个等待锁的地方会立马得到锁资源吗?可以做个实验,给 signal 后 sleep 2秒再调用 unlock

观察打印信息可以看到:

  • 程序先执行 _remove 方法,先加锁,然后遇到 if 条件满足了,则执行 wait 。wait 干的事情是先解锁,然后等待另一个地方发送 signal
  • 然后 _add 方法得到了锁,加锁。开始执行 addObject 方法。然后立马调用 signal 方法
  • 但此时 _remove 方法内的逻辑还没执行在2s后才执行。说明2s后等 _add 方法调用 unlock 方法后,_remove wait 才得到锁资源

结论:如果逻辑很简单,NSCondition unlock 和 signal 的顺序没有要求。但要意识到只发送 signal没有 unlock 的话wait 是不能立马得到锁的,需要等 unlock 后才可以执行后续逻辑。具体顺序看业务场景

存在 虚假唤醒 的问题。则可以将后续的 if 判断换为 while。比如某一时刻发送了一次 signal然后可能有多个线程收到唤醒的信号则可能还是会存在问题。所以 if 换为 while。

NSCondtionLock

NSConditionLock 是对 NSCondition 的进一步封装,可以设置具体的条件值(感兴趣的可以查看 GUN 源码)。

API 如下:

@interface NSConditionLock : NSObject <NSLocking>

- (instancetype)initWithCondition:(NSInteger)condition NS_DESIGNATED_INITIALIZER;

@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (BOOL)tryLock NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (BOOL)tryLockWhenCondition:(NSInteger)condition NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (void)unlockWithCondition:(NSInteger)condition NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (BOOL)lockBeforeDate:(NSDate *)limit NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit NS_SWIFT_UNAVAILABLE_FROM_ASYNC("Use async-safe scoped locking instead");

@property (nullable, copy) NSString *name API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@end

Demo

分析虽然通过3个线程设置了线程的先后顺序但是多线程任务执行的时候到底谁先执行是没办法控制的。但是通过 NSConditionLock lockWhenCondition:* 的能力,可以控制线程的执行顺序。

另外如果初始化设置了 [[NSConditionLock alloc] initWithCondition:1] 但是使用的地方没有用 lockWhenCondition 而是直接用 lock 则会忽略 condition 的值,直接加锁成功。

dispatch_queue(DISPATCH_QUEUE_SERIAL)

使用 GCD 的串行队列,也是可以实现线程同步。

线程同步的本质就是多线程的任务是顺序执行

dispatch_semaphore

使用

semaphore 叫做”信号量”

信号量的初始值,可以用来控制线程并发访问的最大数量

信号量的初始值为1代表同时只允许1条线程访问资源保证线程同步

可以看到打印了20个线程但是我们控制线程最大数量怎么办呢可以用信号量实现。效果如下

dispatch_semaphore_wait 原理

执行 dispatch_semaphore_wait 方法时,

  • 如果信号量的值 > 0则会让信号量的值 -1然后继续向下执行代码

  • 如果信号量的值 <= 0则线程休眠等待等待多久取决于第二个参数直到信号量的值 > 0直到其他的线程任务执行完毕利用 dispatch_semaphore_signalAPI 让信号量的值+1此时继续会让信号量的值 -1然后继续向下执行代码

dispatch_semaphore_signal 函数的作用:让信号量的值 + 1

所以如何让线程同步?设置信号量的值=1即可。保证同一时间只有一个线程任务在执行。代码如下

有趣的实验:

self.semaphore = dispatch_semaphore_create(1);
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);

上面的代码会 crash。因为创建出来信号量为1但是经过 dispatch_semaphore_wait 之后信号量变为0底层会调用到 _dispatch_semaphore_dispose。内部会做判断,就是原始的信号量

void _dispatch_semaphore_dispose(dispatch_object_t dou,
        DISPATCH_UNUSED bool *allow_free){
    dispatch_semaphore_t dsema = dou._dsema;
    if (dsema->dsema_value < dsema->dsema_orig) {
        DISPATCH_CLIENT_CRASH(dsema->dsema_orig - dsema->dsema_value,
                "Semaphore object deallocated while in use");
    }
    _dispatch_sema4_dispose(&dsema->dsema_sema, _DSEMA4_POLICY_FIFO);
}

使用封装

有的时候我们需要在方法内部创建 semaphore ,则可以创建宏

#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
    semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);

使用

- (void)withdrawMoney {
	SemaphoreBegin
	[super withdrawMoney];
	SemaphoreEnd
}

@synchronized

@synchronized 可递归重入的原理分析/线程缓存空间

@synchronized 使用很方便,它是对 pthread_mutex_t 递归锁的封装。Demo 如下

源码剖析

为了探究下实现,开启汇编调试

通过汇编可以看到 @synchronized 底层调用了 objc_sync_enter 方法,其中又调用了 id2dataos_unfair_recursive_lock_lock_with_options 方法。 可以查看 objc4 的源码(笔者的 objc 版本为 objc4-objc4-912.3),查找 objc_sync_enter

// Begin synchronizing on 'obj'. 
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = _objc_sync_enter_kind(obj, SyncKind::atSynchronize);
    if (result != OBJC_SYNC_SUCCESS)
        OBJC_DEBUG_OPTION_REPORT_ERROR(DebugSyncErrors,
            "objc_sync_enter(%p) returned error %d", obj, result);
    return result;
}

int _objc_sync_enter_kind(id obj, SyncKind kind)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, kind, ACQUIRE);
        ASSERT(data);
        data->mutex.lock();
    } else {
        // @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();
        if (DebugNilSync == Fatal)
            _objc_fatal("@synchronized(nil) is fatal");
    }

    return result;
}

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

    bool matches(id matchObject, SyncKind matchKind) {
        ASSERT(matchKind != SyncKind::invalid);
        ASSERT(kind != SyncKind::invalid);
        return object == matchObject && kind == matchKind;
    }
} SyncData;


using recursive_mutex_t = objc_recursive_lock_t;

可以看到 @synchronized 的本质是一个包装了 objc_recursive_lock_t(不同版本的 OBJC ,其内部实现会不同) 的 recursive_mutex_tt C++ 类。

可以发现,如果 @synchronized 参数为nil@synchronized(nil) 调用 objc_sync_nil(),最终什么也不执行。

static SyncData* id2data(id object, SyncKind kind, enum usage why)
{
    ASSERT(kind != SyncKind::invalid);
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    SyncData **listp = &LIST_FOR_OBJ(object);
    SyncData* result = NULL;

#if ENABLE_FAST_CACHE
    // Check per-thread single-entry fast cache for matching object
    bool fastCacheOccupied = NO;
    SyncData *data = syncData;
    if (data) {
        fastCacheOccupied = YES;

        if (data->matches(object, kind)) {
            // Found a match in fast cache.
            result = data;
            if (result->threadCount <= 0  ||  syncLockCount <= 0) {
                _objc_fatal("id2data fastcache is buggy");
            }

            switch(why) {
            case ACQUIRE: {
                ++syncLockCount;
                break;
            }
            case RELEASE:
                if (--syncLockCount == 0) {
                    // remove from fast cache
                    syncData = nullptr;
                    // atomic because may collide with concurrent ACQUIRE
                    AtomicDecrement(&result->threadCount);
                }
                break;
            case CHECK:
                // do nothing
                break;
            }

            return result;
        }
    }
#endif // ENABLE_FAST_CACHE

    // Check per-thread cache of already-owned locks for matching object
    SyncCache *cache = fetch_cache(NO);
    if (cache) {
        unsigned int i;
        for (i = 0; i < cache->used; i++) {
            SyncCacheItem *item = &cache->list[i];
            if (!item->data->matches(object, kind)) 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->list[i] = cache->list[--cache->used];
                    // atomic because may collide with concurrent ACQUIRE
                    AtomicDecrement(&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) {
            if ( p->matches(object, kind) ) {
                result = p;
                // atomic because may collide with concurrent RELEASE
                AtomicIncrement(&result->threadCount);
                goto done;
            }
            if ( (firstUnused == NULL) && (p->threadCount == 0) )
                firstUnused = p;
        }
    
        // no SyncData currently associated with object
        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->kind = kind;
            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->kind = kind;
    result->threadCount = 1;
    new (&result->mutex) recursive_mutex_t(fork_unsafe);
    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->matches(object, kind)) _objc_fatal("id2data is buggy");

#if ENABLE_FAST_CACHE
        if (!fastCacheOccupied) {
            // Save in fast thread cache
            syncData = result;
            syncLockCount = 1;
        } else
#endif // ENABLE_FAST_CACHE
        {
            // 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;
}

传递一个参数 obj经过 id2data 方法得到一个结构体对象,访问结构体对象的成员变量 mutex,然后调用 lock 方法。

可以看到是一个哈希表 StripedMap,哈希表工作原理就是传递一个 key经过哈希算法生成索引然后获取对应的值。

内部维护了一个哈希表,一个对象对应一个锁(所以为了锁的使用正确,加解锁,需要用同一个对象)

另外 recursive_mutex_tt 在初始化的时候传入 OS_UNFAIR_RECURSIVE_LOCK_INIT,看起来也支持递归。所以 @synchronized 的本质是一个递归互斥锁的封装。

各种锁性能对比

性能从高到低:

os_unfair_lock > OSSpinLock > dispatch_semaphore > pthread_mutex > dispatch_queue(DISPATCH_QUEUE_SERIAL) > NSLock > NSCondition > pthread_mutex(recursive) > NSRecursiveLock > @synchronized

自旋锁、互斥锁对比

什么情况适合使用自旋锁?

  • 预计线程等待锁的时间很短假设线程1的任务本来就很短如果使用其他的锁比如还需要互斥锁的话底层实现会调用 sysCall一个休眠一个唤醒这个时间可能比如循环忙等更耗时。所以如果一个线程任务执行时间很短则考虑使用自旋锁会更高效一些。

  • 加锁的代码(临界区)经常被调用,但竞争情况很少发生

  • CPU资源不紧张

  • 多核处理器

什么情况使用互斥锁比较划算?

  • 预计线程等待锁的时间较长

  • 单核处理器一旦使用自旋锁CPU 就很忙了,很少有资源去处理其他逻辑,会卡顿)

  • 临界区有IO操作IO 操作一般占用 CPU 资源较多,互斥锁本身就占用 CPU所以不适合

  • 临界区代码复杂或者循环量大

  • 临界区竞争非常激烈

atomic

源码探究

atomic 用于保证属性 setter、getter 的原子性操作,相当于在 getter 和 setter 内部加了线程同步的锁。

与之相对的是 nonatomic,也就是非原子性的。假设多线程下,针对一个属性的 setter、getter需要自己加锁来保证读写问题。

使用 atomic 则属性类似下面的伪代码

@property (atomic, strong) NSString *name;

- (NSString *)name {
	// 加锁
	// logic
	// 解锁
	return _name;
}

- (void)setName:(NSString *)name {
	// 加锁
	// logic
	_name = name;
	// 解锁
}

用 atomic 修饰就是线程安全的吗?不是的

比如 atomic 修饰了数组,那么对数组指针的读取和赋值(数组的地址修改)是线程安全的,但是数组的操作不是线程安全的,比如增加元素、删除元素、读取元素。

具体实现,可以参考源码 objc4 的 objc-accessors.mm

属性取值逻辑:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
    if (offset == 0) {
        return object_getClass(self);
    }

    // Retain release world
    id *slot = (id*) ((char*)self + offset);
    if (!atomic) return *slot;
        
    // Atomic retain release world
    spinlock_t& slotlock = PropertyLocks.get()[slot];
    slotlock.lock();
    id value = objc_retain(*slot);
    slotlock.unlock();
    
    // for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
    return objc_autoreleaseReturnValue(value);
}

可以看到在获取属性值的时候,判断是不是 atomic

  • 不是 atomic 则直接 return

  • 如果是 atomic则调用自旋锁 slotlock 加锁取值解锁return

属性赋值逻辑:


void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {
        object_setClass(self, newValue);
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
      	// nonatomic直接赋值
        oldValue = *slot;
        *slot = newValue;
    } else {
      	// atomic加自旋锁
        spinlock_t& slotlock = PropertyLocks.get()[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue);
}

void lock() {
    lockdebug_mutex_lock(this);
    // <rdar://problem/50384154>
    uint32_t opts = OS_UNFAIR_LOCK_DATA_SYNCHRONIZATION | OS_UNFAIR_LOCK_ADAPTIVE_SPIN;
    os_unfair_lock_lock_with_options_inline(&mLock, (os_unfair_lock_options_t)opts);
}

可以看到设置属性的时候会判断是不是 atomic

  • atomic 类型,则直接赋值
  • 非 atomic 类型,则先自旋锁加锁、赋值、解锁

它并不能保证使用属性的过程是线程安全的。

QA为什么在 iOS 上几乎没有使用?

因为属性 getter、setter 使用太高频了,另外 atomic 内部实现是自旋锁,自旋锁是忙等,针对移动设备上那寸土寸金的 CPU太奢侈了太耗费性能了。

atomic 并不能保证使用属性的过程是线程安全的

@property (atomic,copy) NSString *name;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(self){
        for (int i = 0; i<100; i++) {
            self.name = @"杭城小刘";
            NSLog(@"线程1  %@",self.name);
        }
    }

});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(self){
        for (int i = 0; i<100; i++) {
            self.name = @"魅影";
            NSLog(@"线程2  %@",self.name);
        }
    }
});

预期:线程 A 打印出来一定是杭城小刘,线程 B 打印出来是魅影。但事实上可能存在乱序。

atomic 仅保证单次读/写的原子性

// 即使属性是 atomic以下代码仍然线程不安全
if (self.atomicValue > 10) { // 步骤1读取
    self.atomicValue = 0;     // 步骤2写入
}
// 线程 A 可能在步骤1后线程 B 修改了 atomicValue导致逻辑错误

无法保护对象内部状态

指针与内容的区别atomic 仅保证指针本身的原子性(如 NSArray * 的赋值),但对象内部的状态(如数组的元素)不受保护。即使属性声明为 atomic,对可变集合的操作仍可能崩溃。

// 线程A
NSMutableArray *array = self.atomicArray; // 原子性读取指针
[array addObject:@"A"];                    // 非原子操作,可能与其他线程冲突

// 线程B
NSMutableArray *array = self.atomicArray; // 原子性读取指针
[array removeAllObjects];                 // 导致线程A的 addObject: 崩溃

总结atomic 是原子属性,它内部实现是针对属性的 setter、getter 进行加锁(早期实现是自旋旋,因为存在问题,后续替换为了 os_unfair_lock。但是事实上在进行多线程编程的时候我们针对数据的操作并不是修改指针本身思考 NSString 的 getter、setter而是操作类似 NSMutableArray、NSDictionary 这样的 case。比如 @property (atomic, strong) NSMutableArray *hobbies; 如果在多线程情况下进行处理,一边生产者添加数据,一边消费者消费数据,则会产生多线程问题。

所以多线程并发编程来说,线程安全是一个系统性问题,无法仅靠声明 atomic 解决。推荐使用锁是一个合理的方案。此外自旋锁不推荐使用,互斥锁中 pthread_mutex 等性能高一些的锁推荐使用。

多线程读写锁

读写的特点

  • 同一时间只能有1个线程进行写的操作只能有1个写
  • 同一时间,允许有多个线程进行读的操作(可以同时读)
  • 同一时间,不允许既有写的操作,又有读的操作(读写不能同时进行)

允许多个线程同时读,但仅允许一个线程写,读写分开的场景,提高读多写少场景的性能

“多读单写”问题,经常用于文件、数据的,频繁读取但写入较少的共享资源(如缓存数据)

iOS 主流方案有:

  • pthread_rwlock读写锁

  • dispatch_barrier_async异步栅栏调用

pthread_rwlock

初始化

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);

Demo

dispatch_barrier_async

多读单写

// 初始化队列
self.queue = dispatch_queue_create("rwqueue", DISPATCH_QUEUE_CONCURRENT);
// 读
dispatch_async(self.queue, ^{

});
// 写
dispatch_barrier_async(self.queue, ^{

});

注意:

  • dispatch_barrier_async 函数传入的并发队列必须是自己通过 dispatch_queue_cretate 创建的
  • 如果传入的是一个串行队列或全局并发队列,那 dispatch_barrier_async 函数便等同于 dispatch_async 函数的效果

上 Demo

栅栏函数拦不住全局队列

Demo

- (void)testBarrierWithGlobalQueue {
    NSLog(@"%s", __func__);
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 100; i++) {
        dispatch_async(queue, ^() {
            NSLog(@"%d", i);
        });
    }
    dispatch_barrier_async(queue, ^() {
        NSLog(@"100");
    });
    dispatch_async(queue, ^() {
        NSLog(@"101");
    });
    NSLog(@"%s", __func__);
}
- (void)testBarrierWithCustomQueue {
    NSLog(@"%s", __func__);
    dispatch_queue_t queue = dispatch_queue_create(0, 0);
    for (int i = 0; i < 100; i++) {
        dispatch_async(queue, ^() {
            NSLog(@"%d", i);
        });
    }
    dispatch_barrier_async(queue, ^() {
        NSLog(@"100");
    });
    dispatch_async(queue, ^() {
        NSLog(@"101");
    });
    NSLog(@"%s", __func__);
}

结论:可以发现 GCD dispatch_barrier_async 栅栏函数,拦不住全局队列,却可以拦住自己创建的普通队列。这是为什么?

全局队列的业务方不只是当前 App 进程,还有一些系统任务(全局并发队列中不仅有开发者的任务,还有系统的任务),如果我们用我们的任务去栏住系统的任务,可能会导致一些未知的错误。栅栏函数对全局并发队列无效,所以我们在开发的时候一定要注意

为什么 dispatch_barrier_async 拦不住全局并发队列

官方文档证明

Apple 官方文档 dispatch_barrier_async 条目中也明确指出:

The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.

栅栏块仅对通过 DISPATCH_QUEUE_CONCURRENT 创建的并发队列有效。在串行队列或全局并发队列中,其行为与普通异步提交的任务相同

系统设计角度
  • 全局并发队列的共享性与系统任务

    • 全局并发队列是系统提供的共享并发队列,被整个进程(甚至系统)的多个模块共同使用。这些队列不仅执行当前应用提交的任务,还可能运行系统级别的后台任务(如日志、框架内部操作等)
    • 如果开发者用栅栏函数拦截全局队列,可能会阻塞系统关键任务,导致不可预见的后果(如死锁、性能下降或功能异常)。因此,苹果从设计上禁用了栅栏对全局队列的支持,避免干扰系统行为
  • 自定义队列的私有性

    自定义并发队列由开发者显式创建,完全由应用控制。所有提交到该队列的任务都是开发者显式添加的,没有外部任务干扰

GCD 源码角度

libdispatch repo 中和 dispatch_barrier_async 有关的2个函数为_dispatch_lane_wakeup_dispatch_lane_barrier_complete

  • _dispatch_lane_wakeup:处理自定义并发队列的任务唤醒逻辑。当检测到 DISPATCH_WAKEUP_BARRIER_COMPLETE 标志时,会调用 _dispatch_lane_barrier_complete,确保栅栏前的任务全部执行完毕后再执行栅栏任务
  • _dispatch_lane_barrier_complete: 具体处理栅栏同步逻辑,括等待队列中现有任务完成、执行栅栏任务、释放后续任务

查看源码:queue.c(队列核心逻辑)、queue_internal.h(队列内部结构定义)、source.c(任务调度逻辑)

下面的代码进行了简化

队列结构

// 队列结构
struct dispatch_queue_s {
    const struct dispatch_queue_vtable_s *do_vtable; // 虚表指针(定义队列操作函数)
    uint32_t dq_atomic_flags;                        // 队列状态标记(如是否并发、是否被栅栏阻塞)
    // ... 其他字段(如任务链表、线程池引用等)
};

// 全局队列和自定义队列的虚表不同
static const struct dispatch_queue_vtable_s _dispatch_queue_global_vtable = { /* 全局队列操作函数 */ };
static const struct dispatch_queue_vtable_s _dispatch_queue_concurrent_vtable = { /* 自定义并发队列操作函数 */ };

调用 dispatch_barrier_async 任务提交:

  • block 封装为 dispatch_continuation_t 对象,并标记 DC_FLAG_BARRIER

    dispatch_barrier_async_f(dispatch_queue_t dq, void *ctxt,
    		dispatch_function_t func) {
    	dispatch_continuation_t dc = _dispatch_continuation_alloc_cacheonly();
    	uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;	// 标记为栅栏函数
    	dispatch_qos_t qos;
    
    	if (likely(!dc)) {
    		return _dispatch_async_f_slow(dq, ctxt, func, 0, dc_flags);
    	}
    
    	qos = _dispatch_continuation_init_f(dc, dq, ctxt, func, 0, dc_flags);
    	_dispatch_continuation_async(dq, dc, qos, dc_flags);
    }
    
  • 将任务加入到队列。通过队列的 dq_push 方法将任务加入到队列任务链表里

    static inline void
    _dispatch_continuation_async(dispatch_queue_class_t dqu,
    		dispatch_continuation_t dc, dispatch_qos_t qos, uintptr_t dc_flags)
    {
    #if DISPATCH_INTROSPECTION
    	if (!(dc_flags & DC_FLAG_NO_INTROSPECTION)) {
    		_dispatch_trace_item_push(dqu, dc);
    	}
    #else
    	(void)dc_flags;
    #endif
    	return dx_push(dqu._dq, dc, qos); // 调用队列的入队函数
    }
    
    
  • 自定义并发队列dx_push 指向 _dispatch_lane_push,将任务插入队列的任务链表尾部

  • 全局队列dx_push 指向 _dispatch_root_queue_push,直接忽略 DC_FLAG_BARRIER 标记

队列唤醒与任务调度

当队列需要执行任务时GCD 会调用队列的 dx_wakeup 方法。不同队列的唤醒逻辑存在差异:

  • 自定义并发队列的唤醒逻辑。自定义队列的 dx_wakeup 指向 _dispatch_lane_wakeup,其关键逻辑如下

    // 自定义队列唤醒函数queue.c
    static void _dispatch_lane_wakeup(dispatch_lane_class_t dq, ...) {
        // 检查队列状态和栅栏标记
        if (dq->dq_atomic_flags & DC_FLAG_BARRIER) {
            // 进入栅栏同步逻辑
            _dispatch_lane_barrier_complete(dq);
        } else {
            // 普通任务调度
            _dispatch_lane_drain(dq);
        }
    }
    
    static void _dispatch_lane_barrier_complete(dispatch_lane_class_t dq, ...) {
        // 1. 等待前置任务完成
        while (存在未完成的非栅栏任务) {
            _dispatch_lane_drain_non_barriers(dq); // 执行所有非栅栏任务
        }
    
        // 2. 执行栅栏任务
        dispatch_continuation_t barrier_dc = 从队列中取出栅栏任务;
        _dispatch_client_callout(barrier_dc->dc_func); // 执行栅栏块
    
        // 3. 释放后续任务
        _dispatch_lane_class_barrier_complete(dq); // 清除栅栏标记,唤醒后续任务
        _dispatch_lane_drain(dq); // 继续执行后续任务
    }
    
  • 全队队列的唤醒逻辑

    全局队列的 dx_wakeup 指向 _dispatch_root_queue_wakeup,其逻辑完全忽略栅栏标记

    DISPATCH_OPTIONS(dispatch_wakeup_flags, uint32_t,
    	// The caller of dx_wakeup owns two internal refcounts on the object being
    	// woken up. Two are needed for WLH wakeups where two threads need
    	// the object to remain valid in a non-coordinated way
    	// - the thread doing the poke for the duration of the poke
    	// - drainers for the duration of their drain
    	DISPATCH_WAKEUP_CONSUME_2               = 0x00000001,
    
    	// Some change to the object needs to be published to drainers.
    	// If the drainer isn't the same thread, some scheme such as the dispatch
    	// queue DIRTY bit must be used and a release barrier likely has to be
    	// involved before dx_wakeup returns
    	DISPATCH_WAKEUP_MAKE_DIRTY              = 0x00000002,
    
    	// This wakeup is made by a sync owner that still holds the drain lock
    	DISPATCH_WAKEUP_BARRIER_COMPLETE        = 0x00000004,
    
    	// This wakeup is caused by a dispatch_block_wait()
    	DISPATCH_WAKEUP_BLOCK_WAIT              = 0x00000008,
    
    	// This wakeup may cause the source to leave its DSF_NEEDS_EVENT state
    	DISPATCH_WAKEUP_EVENT                   = 0x00000010,
    
    	// This wakeup is allowed to clear the ACTIVATING state of the object
    	DISPATCH_WAKEUP_CLEAR_ACTIVATING        = 0x00000020,
    );
    
    
    void _dispatch_root_queue_wakeup(dispatch_queue_global_t dq,
    		DISPATCH_UNUSED dispatch_qos_t qos, dispatch_wakeup_flags_t flags)
    {
    	if (!(flags & DISPATCH_WAKEUP_BLOCK_WAIT)) {
    		DISPATCH_INTERNAL_CRASH(dq->dq_priority,
    				"Don't try to wake up or override a root queue");
    	}
    	if (flags & DISPATCH_WAKEUP_CONSUME_2) { // 只处理 DISPATCH_WAKEUP_CONSUME_2 类型,忽略 DISPATCH_WAKEUP_BARRIER_COMPLETE 
    		return _dispatch_release_2_tailcall(dq);
    	}
    }
    

可以看到:_dispatch_root_queue_wakeup:全局队列的唤醒函数。此函数未处理 DC_FLAG_BARRIER 标记,直接忽略栅栏逻辑,按默认并发方式执行任务。因此,全局队列无法支持栅栏功能

总结dispatch_barrier_async 的设计初衷是为开发者控制的私有并发队列提供同步机制,避免全局队列的共享性引入风险。因此,务必仅将栅栏用于自定义的并发队列

dispatch_group_async

如何实现 A、B、C 三个任务并发执行完,再去执行任务 D ?假设需求是根据省市区下载 json然后根据 json 数据,选中地址 picker view。

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("com.unix.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);
for (NSURL *url in addressArray) {
	dispatch_group_async(group, queue, ^{
			// 根据 url 请求 json 数据
  });
}

// 会等到上面加入到 group 的3个并发任务全部执行完再执行下面的 block 任务
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
	// 根据下载好的省市区 json 去更新 picker view 。
});

六、NSOperation

需要和 NSOperationQueue 配合使用。优点:

  • 可以添加任务依赖

  • 任务执行状态控制

    • isReady
    • isExecuting
    • isFinished
    • isCancelled

    如果只重写了 main 方法,底层控制变更任务执行完成状态,以及任务退出。

    如果重写了 start 方法,自行控制任务状态

  • 最大并发量

系统是怎么样移除一个 isFinished = YES 的 NSOperation 的?

看看 GNU 源码。

- (void) start
{
  ENTER_POOL
	// 获取线程优先级
  double	prio = [NSThread  threadPriority];

  AUTORELEASE(RETAIN(self));	// Make sure we exist while running.
  [internal->lock lock];
  NS_DURING
    {
    // 做一些状态判断
      if (YES == [self isExecuting])
	{
	  [NSException raise: NSInvalidArgumentException
		      format: @"[%@-%@] called on executing operation",
	    NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
	}
      if (YES == [self isFinished])
	{
	  [NSException raise: NSInvalidArgumentException
		      format: @"[%@-%@] called on finished operation",
	    NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
	}
      if (NO == [self isReady])
	{
	  [NSException raise: NSInvalidArgumentException
		      format: @"[%@-%@] called on operation which is not ready",
	    NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
	}
      if (NO == internal->executing)
	{
		// 如果调用 start 方法,通过 KVO 将 isExecuting 更改为 YES
	  [self willChangeValueForKey: @"isExecuting"];
	  internal->executing = YES;
	  [self didChangeValueForKey: @"isExecuting"];
	}
    }
  NS_HANDLER
    {
      [internal->lock unlock];
      [localException raise];
    }
  NS_ENDHANDLER
  [internal->lock unlock];

  NS_DURING
    {
      if (NO == [self isCancelled])
	{
	  [NSThread setThreadPriority: internal->threadPriority];
     // 内部调用 main 方法
	  [self main];
	}
    }
  NS_HANDLER
    {
      [NSThread setThreadPriority:  prio];
      [localException raise];
    }
  NS_ENDHANDLER;
	// 调用完 start内部调用 finish 方法
  [self _finish];
  LEAVE_POOL
}

可以看到,内部会调用 _finish 方法

- (void) _finish
{
  [internal->lock lock];
  if (NO == internal->finished)
    {
      if (YES == internal->executing)
        {
	  [self willChangeValueForKey: @"isExecuting"];
	  [self willChangeValueForKey: @"isFinished"];
	  internal->executing = NO;
	  internal->finished = YES;
	  [self didChangeValueForKey: @"isFinished"];
	  [self didChangeValueForKey: @"isExecuting"];
	}
      else
	{
	  [self willChangeValueForKey: @"isFinished"];
	  internal->finished = YES;
	  [self didChangeValueForKey: @"isFinished"];
	}
      if (NULL != internal->completionBlock)
	{
	  CALL_BLOCK_NO_ARGS(
	    ((GSOperationCompletionBlock)internal->completionBlock));
	}
    }
  [internal->lock unlock];
}

可以看到在 _finish 方法中,系统通过 KVO 移除 NSOperationQueue 中 NSOperation 的。

七、NSThread

NSThread 的一个工作流程如下:

start() -> 创建 pthread -> main() -> [target performSelector:selector] -> exit

NSThread 需要保活。为什么会死掉?看看 gnu 源码

- (void) start
{
  pthread_attr_t	attr;

  if (_active == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on active thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_cancelled == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on cancelled thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }
  if (_finished == YES)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on finished thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  /* Make sure the notification is posted BEFORE the new thread starts.
   */
  gnustep_base_thread_callback();

  /* The thread must persist until it finishes executing.
   */
  RETAIN(self);

  /* Mark the thread as active while it's running.
   */
  _active = YES;

  errno = 0;
  pthread_attr_init(&attr);
  /* Create this thread detached, because we never use the return state from
   * threads.
   */
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
  /* Set the stack size when the thread is created.  Unlike the old setrlimit
   * code, this actually works.
   */
  if (_stackSize > 0)
    {
      pthread_attr_setstacksize(&attr, _stackSize);
    }
  // 设置回调函数
  if (pthread_create(&pthreadID, &attr, nsthreadLauncher, self))
    {
      DESTROY(self);
      [NSException raise: NSInternalInconsistencyException
                  format: @"Unable to detach thread (last error %@)",
                  [NSError _last]];
    }
}

看看 pthread 创建后的回调函数

static void *
nsthreadLauncher(void *thread)
{
  NSThread *t = (NSThread*)thread;

  setThreadForCurrentThread(t);

  /*
   * Let observers know a new thread is starting.
   */
  if (nc == nil)
    {
      nc = RETAIN([NSNotificationCenter defaultCenter]);
    }
  // 发送通知
  [nc postNotificationName: NSThreadDidStartNotification
		    object: t
		  userInfo: nil];
	// 设置线程名
  [t _setName: [t name]];
	// 调用 main 方法
  [t main];
	// 线程退出
  [NSThread exit];
  // Not reached
  return NULL;
}

看了源码,会发现 NSThread 调用 start 内部就会调用 [NSThread exit] 所以会退出。要想常驻,就需要在 main 方法做 runloop 保活。

- (void) main
{
  if (_active == NO)
    {
      [NSException raise: NSInternalInconsistencyException
                  format: @"[%@-%@] called on inactive thread",
        NSStringFromClass([self class]),
        NSStringFromSelector(_cmd)];
    }

  [_target performSelector: _selector withObject: _arg];
}

main 方法内其实就是在执行 _selector。也就是在 NSThread 的初始化方法中,传入的 selector 中进行 runloop 保活逻辑。

八、其他常见的多线程编程模式

Promise

Promise 在多线程解决方案中比较常见,比如在前端中 Promise 就是一个标准解决方案。同样的iOS 界也有三方开发者写的 PromiseKit。也有对应的 AFNetworking Promise 版本。

Promise 解决了什么问题?

  • 在需要多个操作的时候我们可能会设置多个回调参数嵌套导致代码很长也就是传说中的“回调地狱”Callback Hell

  • 丧失了 return 特性

Promise 就是一个对象,用来传递异步操作的消息。代表了某个未来才会知道结果的事件(也就是异步操作),并且这个事件提供统一的 API可以供进一步处理

对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中,又称 Incomplete)、Resolved(已完成,又称 Fulfilled)和 Rejected (已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。

一旦状态改变就不会再变任何时候都可以得到这个结果。Promise对象的状态改变只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生状态就凝固了不会再变了会一直保持这个结果。就算改变已经发生了你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

APIClient.fetchData(...).then().onFailure();

Pipeline

将一个任务分解为若干个阶段(Stage),前阶段的输出为下阶段的输入,各个阶段由不同的工

作者线程负责执行。

各个任务的各个阶段是并行(Parallel)处理的。

具体任务的处理是串行的,即完成一个任务要依次执行各个阶段,但从整体任务上看,不同任务的各个阶段的执行是并行的。

Master-Slave

将一个任务分解为若干个语义等同的子任务,并由专门的工作者线程来并行执行这些子任务,既 提高计算效率,又实现了信息隐藏。

比如 Jekins

Serial Thread Confinement

如果并发任务的执行涉及某个非线程安全对象,而很多时候我们又不希望因此而引入锁。

通过将多个并发的任务存入队列实现任务的串行化,并为这些串行化任务创建唯一的工作者线程进行处理。

比如 FMDB 的设计,内部就是一个串行队列。

总结

  • 怎么样实现多读单写GCD dispatch_barrier_async
  • iOS 提供了几种多线程技术
    • GCD简单的任务处理以及多读单写、读写锁、dispatch_group_async
    • NSOperationQueue、NSOperationAFNetworking、SDWebImage ,可以方便对 Operation 的状态管理和依赖管理
    • NSThread主要用于实现常驻线程
  • NSOperation Finished 之后如何移除KVO
  • 锁操作的返回值检查不是 “可选的”,而是 “必须的”,区别仅在于「失败后是崩溃、打日志还是抛异常」,需根据组件的核心程度选择。
    • 系统级开源库(如 libdispatch严格检查返回值非预期失败直接崩溃保证系统稳定性
    • 第三方开源库AFNetworking/SDWebImage调试阶段断言 + Release 阶段日志(平衡调试和线上稳定性);
    • 业务组件(如你的字典):推荐「断言 + 日志」模式 ——Debug 暴露问题Release 不崩溃且留痕;