iOS底层原理 多线程

17 阅读9分钟

一、基础概念

iOS中多线程的技术。

  • pthread
  • NSOperation(底层是GCD)
  • NSThread
  • GCD
  1. 进程:一个 App 就是一个进程,独立内存空间。
  2. 线程:进程内执行任务的最小单元,共享进程堆内存;栈是每个线程独立。

主线程(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 大类

  1. 串行队列 Serial:一个线程,任务排队顺序执行
  2. 并发队列 Concurrent:多个线程,任务并发执行

系统自带队列:

  • 主队列 dispatch_get_main_queue():串行,绑定主线程
  • 全局并发队列 dispatch_get_global_queue(优先级,0)

任务 2 种

  • dispatch_sync 同步:阻塞当前线程,等任务执行完才返回(死锁重点考点)
  • dispatch_async 异步:不阻塞当前线程,任务丢到队列,函数立刻返回

🔥 经典死锁:主线程调用 sync 往主队列追加任务 主线程正在等 sync 完成;主队列等待主线程空闲执行新任务 → 互相等待死锁。

GCD 高级 API

  1. dispatch_group 任务组:监听一批并发任务全部完成
  2. dispatch_barrier 栅栏:并发队列中,barrier 任务必须等前面全部做完,后面等 barrier 做完才执行(读写锁常用)
  3. dispatch_once 单例(线程安全,Swift 废弃,OC 经典)
  4. dispatch_semaphore 信号量:控制最大并发数量,也可当做锁
  5. 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 / 数据错乱。 锁从性能从高到低排序:

  1. OSSpinLock 自旋锁。OSSpinLock(⚠️ iOS10 已废弃,生产禁止使用)
  2. os_unfair_lock (推荐,iOS10+,自旋锁,等待时忙等,不陷入内核休眠,⚠️不能递归)
  3. pthread_mutex_t 互斥锁,支持递归锁,等待时线程休眠,CPU 开销低
  4. NSLock 封装 pthread_mutex,OC 对象
  5. @synchronized 语法糖,底层递归锁,性能最差,方便简单场景
  6. pthread_rwlock_t 读写锁:读可并发,写互斥(适合多读少写场景)
  7. dispatch_semaphore 信号量当做锁
  8. NSCondition
  9. NSConditionLock
  10. NSRecursiveLock

四、RunLoop 和多线程联动(你重点研究这块)

  1. 主线程 RunLoop 默认开启;新建子线程,默认没有 RunLoop,执行完直接销毁

  2. RunLoop 作用:线程保活 + 事件分发(Source0/Source1/Timer/Observer)

  3. 常驻子线程套路:

    • NSThread + NSPort(占位 source,防止 mode 为空)
    • while 循环调用 runMode:beforeDate:
    • 原子布尔标记 isStop 控制退出
    • CFRunLoopStop + CFRunLoopWakeUp 唤醒退出(前面我们分析的整套源码)

关键点:RunLoop 只是循环,本身不是多线程;RunLoop 属于某一个特定线程,一个线程最多一个 RunLoop。

五、常见坑 & 面试题

  1. 子线程更新 UI:不直接崩溃,但是 UI 渲染错乱、偶现 crash。底层 UIKit 不是线程安全,必须切回主线程。

  2. NSTimer 在子线程:必须加到当前线程 RunLoop,否则不触发;RunLoop 休眠时 timer 不会执行。

  3. 循环引用:GCD block 捕获 self 容易循环引用,weakSelf + strongSelf 套路。

  4. 同步异步 + 串行并发的 4 种组合题(必考)

    • 异步 + 串行:新开子线程,顺序执行
    • 同步 + 串行:阻塞当前线程
  5. 常驻子线程退出:不能直接 [thread cancel] 强制杀死,要软标记,等待当前任务执行完毕退出。强制杀死线程会资源泄漏。

  6. atomic:仅保证属性 get/set 原子,不能保证业务线程安全,只能防止半读写,不能解决多个操作组合的竞态。

六、GCD 和 NSOperation 对比

表格

GCDNSOperation
底层C 语言 API,内核层OC 封装 GCD
依赖需要 group 手写原生 addDependency
取消block 提交后不可取消支持 cancel
监听完成group notifyKVO isFinished
使用场景简单异步、高性能复杂任务编排、可取消任务

七、简单选型建议

  1. 简单异步网络、后台计算 → GCD async
  2. 任务之间有依赖、需要随时取消 → NSOperationQueue
  3. 需要长期常驻后台线程(SDK、独立任务线程)→ NSThread + RunLoop(你现在这套)
  4. 多线程读写共享变量 → 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);
}

三、面试手写问答题

  1. 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 往同一个串行队列追加任务,互相等待。

  1. 常驻线程为什么需要 NSPort?

RunLoop 如果 mode 里面没有 source/timer,__CFRunLoopModeIsEmpty为 true,runMode 直接返回 Finished,while 循环退出。添加 NSPort 提供 source1,保证 mode 不为空,RunLoop 可以进入等待休眠。

  1. CFRunLoopStop 什么时候生效?

设置_stopped=true标记;不会中断正在执行的 source 回调,等本轮事件处理完成,回到 while 循环头部,检测标记退出;线程休眠时必须 CFRunLoopWakeUp 唤醒,才能读取标记。

  1. 多线程和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")会慢。