一、基础概念
iOS中多线程的技术。
- pthread
- NSOperation(底层是GCD)
- NSThread
- GCD
- 进程:一个 App 就是一个进程,独立内存空间。
- 线程:进程内执行任务的最小单元,共享进程堆内存;栈是每个线程独立。
主线程(UI 线程):App 启动默认创建,UI 必须在主线程更新,主线程有自带 RunLoop。 子线程:默认没有 RunLoop,执行完任务就销毁,如果要常驻子线程,必须手动开启 RunLoop(你之前 MyThread 保活代码)。
线程生命周期
新建 → 就绪 → 运行 → 阻塞 → 销毁 子线程执行完入口函数,线程直接退出;RunLoop 本质就是让线程不退出,循环等待事件。
二、4 套 API 对比
1. pthread(C 语言,底层)
- 跨平台,手动管理线程创建、销毁、锁
pthread_t tid;
pthread_create(&tid, NULL, runFunc, NULL);
缺点:全手动,容易内存泄漏,iOS 业务极少直接使用。
2. NSThread(OC 封装 pthread)
- 面向对象,可以拿到线程对象,可手动启动 / 取消
NSThread *thread = [[NSThread alloc] initWithBlock:^{
//子线程任务
}];
[thread start];
特点:
- 可以监听
thread.isCancelled做退出标记 - 子线程默认无 RunLoop,需要手动添加 port+runMode 循环保活
NSThread的内存泄漏
NSThread持有self。所以Thread结束之前self都不会释放。
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor greenColor];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(myPrint) object:nil];
[thread start];
}
- (void)myPrint{
for (int i=0; i<1000; i++) {
NSLog(@"myPrint ---%d %@",i,@"self");
[NSThread sleepForTimeInterval:1.0];
}
}
可以主动释放NSThread。通过cancel方法。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(myPrint) object:nil];
[self.myThread start];
}
- (void)myPrint{
for (int i=0; i<1000; i++) {
if ([NSThread currentThread].isCancelled){
break;
}
NSLog(@"myPrint ---%d %@",i,@"self");
[NSThread sleepForTimeInterval:1.0];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 只是标记需要cancel
[self.myThread cancel];
[self dismissViewControllerAnimated:YES completion:nil];
}
或者通过NSProxy。本质是self强引用中间对象。中间对象弱引用self。Thread引用的是中间对象。实际效果并不好。如果不cancel,还是会内存泄漏。
detach
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor greenColor];
__weak typeof(self) weakSelf = self;
// 这里不会发生内存泄漏
// [NSThread detachNewThreadWithBlock:^{
// for (int i=0; i<1000; i++) {
// NSLog(@"myPrint ---%d %@",i,@"self");
// [NSThread sleepForTimeInterval:1.0];
// }
//
// }];
// 这里也不会循环引用
// [NSThread detachNewThreadWithBlock:^{
// for (int i=0; i<1000; i++) {
// NSLog(@"myPrint ---%d %@",i,@"self");
// [weakSelf print];
// [weakSelf print1];
// [NSThread sleepForTimeInterval:1.0];
// }
//
// }];
// 内存泄漏
[NSThread detachNewThreadWithBlock:^{
[weakSelf print2];
}];
}
- (void)print{
NSLog(@"---");
}
- (void)print1{
NSLog(@"my print begin");
[NSThread sleepForTimeInterval:1];
NSLog(@"my print finished");
}
- (void)print2{
for (int i=0; i<1000; i++) {
NSLog(@"print2 %d", i);
[NSThread sleepForTimeInterval:2];
}
}
3. GCD(最常用,系统托管线程池)
核心:队列 + 任务,自动管理线程复用,不用手动创建销毁线程。
队列 2 大类
- 串行队列 Serial:一个线程,任务排队顺序执行
- 并发队列 Concurrent:多个线程,任务并发执行
系统自带队列:
- 主队列
dispatch_get_main_queue():串行,绑定主线程- 全局并发队列
dispatch_get_global_queue(优先级,0)
任务 2 种
dispatch_sync同步:阻塞当前线程,等任务执行完才返回(死锁重点考点)dispatch_async异步:不阻塞当前线程,任务丢到队列,函数立刻返回
🔥 经典死锁:主线程调用
sync 往主队列追加任务主线程正在等 sync 完成;主队列等待主线程空闲执行新任务 → 互相等待死锁。
GCD 高级 API
dispatch_group任务组:监听一批并发任务全部完成dispatch_barrier栅栏:并发队列中,barrier 任务必须等前面全部做完,后面等 barrier 做完才执行(读写锁常用)dispatch_once单例(线程安全,Swift 废弃,OC 经典)dispatch_semaphore信号量:控制最大并发数量,也可当做锁dispatch_source:定时器(比 NSTimer 准,不受 RunLoop 模式影响)
4. NSOperation / NSOperationQueue(GCD 上层 OC 封装)
- NSOperation:任务对象(NSInvocationOperation / NSBlockOperation / 自定义子类重写 main)
- NSOperationQueue:队列,底层 GCD 优势: ✅ 支持任务依赖(addDependency) ✅ 可以取消任务
cancel✅ 可以设置最大并发数maxConcurrentOperationCount✅ KVO 监听任务状态:isReady /isExecuting/isFinished
区别 GCD:GCD 任务是 block,一旦提交无法取消;NSOperation 可以 cancel。
三、线程安全(锁,高频面试)
多线程同时读写同一个可变资源会产生竞态条件 (race condition) ,crash / 数据错乱。 锁从性能从高到低排序:
OSSpinLock自旋锁。OSSpinLock(⚠️ iOS10 已废弃,生产禁止使用)os_unfair_lock(推荐,iOS10+,自旋锁,等待时忙等,不陷入内核休眠,⚠️不能递归)pthread_mutex_t互斥锁,支持递归锁,等待时线程休眠,CPU 开销低NSLock封装 pthread_mutex,OC 对象@synchronized语法糖,底层递归锁,性能最差,方便简单场景pthread_rwlock_t读写锁:读可并发,写互斥(适合多读少写场景)dispatch_semaphore信号量当做锁NSConditionNSConditionLockNSRecursiveLock
四、RunLoop 和多线程联动(你重点研究这块)
-
主线程 RunLoop 默认开启;新建子线程,默认没有 RunLoop,执行完直接销毁
-
RunLoop 作用:线程保活 + 事件分发(Source0/Source1/Timer/Observer)
-
常驻子线程套路:
- NSThread + NSPort(占位 source,防止 mode 为空)
- while 循环调用
runMode:beforeDate: - 原子布尔标记 isStop 控制退出
- CFRunLoopStop + CFRunLoopWakeUp 唤醒退出(前面我们分析的整套源码)
关键点:RunLoop 只是循环,本身不是多线程;RunLoop 属于某一个特定线程,一个线程最多一个 RunLoop。
五、常见坑 & 面试题
-
子线程更新 UI:不直接崩溃,但是 UI 渲染错乱、偶现 crash。底层 UIKit 不是线程安全,必须切回主线程。
-
NSTimer在子线程:必须加到当前线程 RunLoop,否则不触发;RunLoop 休眠时 timer 不会执行。 -
循环引用:GCD block 捕获 self 容易循环引用,
weakSelf + strongSelf套路。 -
同步异步 + 串行并发的 4 种组合题(必考)
- 异步 + 串行:新开子线程,顺序执行
- 同步 + 串行:阻塞当前线程
-
常驻子线程退出:不能直接
[thread cancel]强制杀死,要软标记,等待当前任务执行完毕退出。强制杀死线程会资源泄漏。 -
atomic:仅保证属性 get/set 原子,不能保证业务线程安全,只能防止半读写,不能解决多个操作组合的竞态。
六、GCD 和 NSOperation 对比
表格
| GCD | NSOperation | |
|---|---|---|
| 底层 | C 语言 API,内核层 | OC 封装 GCD |
| 依赖 | 需要 group 手写 | 原生 addDependency |
| 取消 | block 提交后不可取消 | 支持 cancel |
| 监听完成 | group notify | KVO isFinished |
| 使用场景 | 简单异步、高性能 | 复杂任务编排、可取消任务 |
七、简单选型建议
- 简单异步网络、后台计算 → GCD async
- 任务之间有依赖、需要随时取消 → NSOperationQueue
- 需要长期常驻后台线程(SDK、独立任务线程)→ NSThread + RunLoop(你现在这套)
- 多线程读写共享变量 → os_unfair_lock /pthread_mutex
示例 1:GCD 同步异步 + 串行并发
// 全局并发队列
dispatch_queue_t globalQ = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
// 自定义串行队列
dispatch_queue_t serialQ = dispatch_queue_create("com.test.serial", DISPATCH_QUEUE_SERIAL);
// 异步并发
dispatch_async(globalQ, ^{
NSLog(@"并发任务1 %@", [NSThread currentThread]);
});
// 同步任务:阻塞当前线程
dispatch_sync(serialQ, ^{
NSLog(@"串行同步任务");
});
// ❌ 经典死锁代码(不要运行!)
// dispatch_sync(dispatch_get_main_queue(), ^{
// NSLog(@"死锁");
// });
示例 2:dispatch_group 任务组,等待一批任务全部完成
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
sleep(1);
NSLog(@"任务1完成");
});
dispatch_group_async(group, queue, ^{
sleep(2);
NSLog(@"任务2完成");
});
// 全部完成后回调(会切到指定队列,一般主队列)
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"✅ 所有任务执行完毕,回到主线程更新UI");
});
示例 3:dispatch_barrier 栅栏(多读单写)
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.test.barrier", DISPATCH_QUEUE_CONCURRENT);
// 读:并发执行
dispatch_async(concurrentQueue, ^{ NSLog(@"读A"); });
dispatch_async(concurrentQueue, ^{ NSLog(@"读B"); });
// barrier:等前面全部结束,才执行barrier,后面任务等barrier结束
dispatch_barrier_async(concurrentQueue, ^{
NSLog(@"🔒写操作,独占队列");
});
dispatch_async(concurrentQueue, ^{ NSLog(@"读C"); });
示例 4:dispatch_semaphore 信号量(控制并发数 / 简易锁)
// 参数=2,最多允许2个并发
dispatch_semaphore_t sem = dispatch_semaphore_create(2);
dispatch_queue_t q = dispatch_get_global_queue(0,0);
for(int i=0;i<5;i++){
dispatch_async(q, ^{
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // -1,占用信号量
NSLog(@"任务%d开始",i);
sleep(1);
NSLog(@"任务%d结束",i);
dispatch_semaphore_signal(sem); // +1,释放
});
}
示例 5:NSOperation + 任务依赖
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 2;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
sleep(1);
NSLog(@"任务1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"任务2,依赖任务1完成才执行");
}];
// 添加依赖:op2必须等op1完成
[op2 addDependency:op1];
[queue addOperation:op1];
[queue addOperation:op2];
示例 6:常驻 NSThread + RunLoop
#import <Foundation/Foundation.h>
@interface MyWorkerThread ()
@property (nonatomic, strong, nullable) NSThread *thread;
@property (atomic, assign) BOOL isStop;
@property (nonatomic, assign) CFRunLoopRef threadRunLoop;
@end
@implementation MyWorkerThread
- (void)start {
self.isStop = NO;
self.thread = [[NSThread alloc] initWithBlock:^{
self.threadRunLoop = CFRunLoopGetCurrent();
NSLog(@"子线程启动");
// 添加RunLoop状态监听Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop:进入循环");break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop:准备休眠");break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop:被唤醒");break;
case kCFRunLoopExit:
NSLog(@"RunLoop:即将退出");break;
default:break;
}
});
CFRunLoopAddObserver(self.threadRunLoop, observer, kCFRunLoopDefaultMode);
CFRelease(observer);
// 占位NSPort,防止mode为空直接退出
NSPort *keepPort = [[NSPort alloc] init];
[[NSRunLoop currentRunLoop] addPort:keepPort forMode:NSDefaultRunLoopMode];
while (!self.isStop) {
// 单次RunLoop迭代
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
[[NSRunLoop currentRunLoop] removePort:keepPort forMode:NSDefaultRunLoopMode];
NSLog(@"子线程退出");
}];
[self.thread start];
}
- (void)stopThread {
NSLog(@"准备停止RunLoop");
self.isStop = YES;
CFRunLoopStop(self.threadRunLoop);
CFRunLoopWakeUp(self.threadRunLoop); // 唤醒休眠的RunLoop,读取stop标记
}
// 在子线程执行任务
- (void)executeTask:(void(^)(void))block {
[self performSelector:block onThread:self.thread withObject:nil waitUntilDone:NO];
}
@end
示例 7:os_unfair_lock 线程安全锁
#import <os/lock.h>
os_unfair_lock _lock;
- (void)threadSafeWrite {
os_unfair_lock_lock(&_lock);
// 读写共享资源
os_unfair_lock_unlock(&_lock);
}
三、面试手写问答题
- GCD sync + async 4 种场景输出题
dispatch_queue_t q = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
NSLog(@"start");
dispatch_async(q, ^{
NSLog(@"async1");
dispatch_sync(q, ^{
NSLog(@"sync1");
});
NSLog(@"async1 end");
});
NSLog(@"end");
答案:start → end → async1,然后死锁。串行队列里,异步任务内部 sync 往同一个串行队列追加任务,互相等待。
- 常驻线程为什么需要 NSPort?
RunLoop 如果 mode 里面没有 source/timer,
__CFRunLoopModeIsEmpty为 true,runMode 直接返回 Finished,while 循环退出。添加 NSPort 提供 source1,保证 mode 不为空,RunLoop 可以进入等待休眠。
- CFRunLoopStop 什么时候生效?
设置
_stopped=true标记;不会中断正在执行的 source 回调,等本轮事件处理完成,回到 while 循环头部,检测标记退出;线程休眠时必须 CFRunLoopWakeUp 唤醒,才能读取标记。
- 多线程和Runloop
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
[self performSelector:@selector(test1) withObject:nil afterDelay:0];
[self performSelector:@selector(test2) onThread:[NSThread mainThread] withObject:nil waitUntilDone:NO];
NSLog(@"3");
});
}
- (void)test1{
NSLog(@"test1 2");
}
- (void)test2{
NSLog(@"test2 2");
}
输出
1
3
test2 2
其中test1并不会执行,因为子线程默认runloop。所以 [self performSelector:@selector(test1) withObject:nil afterDelay:0];这个代码等于往子线程添加一个定时器,定时器依赖runloop执行。
test2可以执行,且在最后,是因为主线程的runloop里面添加了一个任务。因此执行速度比NSLog(@"3")会慢。