[OC 底层] (四) 多线程相关内容
文章目录
-
- [[OC 底层] (四) 多线程相关内容](#[OC 底层] (四) 多线程相关内容)
- 前言
- 多线程原理
-
- 多线程
- 队列与函数
- GCD的基本使用
- [GCD 相关函数](#GCD 相关函数)
-
- dispatch_after
- dispatch_once
- dispatch_apply
- dispatch_group_t
- [dispatch_barrier_sync & dispatch_barrier_async](#dispatch_barrier_sync & dispatch_barrier_async)
- dispatch_semaphore_t
- dispatch_source_t
- 小结
前言
多线程相关内容是开发里非常重要的一部分,掌握它就相当于打开了新世界的大门。
多线程原理
在这里我们先从进程和线程讲起
进程
进程指的是系统中正在运行的一个应用程序。它具有几个关键特征:
- 独立性:每个进程之间是相互独立的,彼此之间互不干扰。例如在手机上同时运行的支付宝、微信、QQ,它们就分别属于不同的进程。
- 专属内存空间:每个进程都运行在其专用的、受保护的内存空间内,其他进程无法直接访问。
- 资源拥有者 :进程拥有自己的内存、I/O、CPU 时间片等系统资源。
在 macOS 中,可以通过"活动监视器"查看系统当前运行的所有进程;在 iOS 中则是单进程开发模型,一个 App 就是一个进程。
线程
线程是进程的基本执行单元,进程中所有的任务都必须在线程中执行。它的核心特点包括:
- 依附性:线程不能独立存在,必须依存于某个进程中,由应用程序提供调度控制。
- 必要性 :进程要执行任务,至少要有一条线程。程序启动时会默认开启一条线程,称为主线程(也叫 UI 线程)。
- 调度单位:线程是处理器(CPU)调度的基本单位,而进程不是。
- 共享地址空间:线程本身没有独立的地址空间,它包含在所属进程的地址空间中。
线程和进程的关系
进程相当于一个工厂,线程相当于工厂中的一条流水线,这也就是说进程是线程的容器 ,而线程是真正干活的执行体。
了解完进程和线程以后我们就可以了解什么是多线程了。
多线程
如果说我们的 CPU 是单核的话,同一时间 CPU 只能处理一条线程,即只有一条线程在工作。
如果 CPU 是现代的 A19 或者 M5 的话,多个核心可以真正并行地运行多条线程,再叠加每个核心内部的时间片切换,形成更高效的并发能力。但调度的基本逻辑没有变------线程依然是 CPU 调度的最小单位。
优点
- 能适当
提高程序的执行效率 - 能适当
提高资源的利用率,如CPU、内存 - 线程上的任务执行完成后,
线程会自动销毁
缺点
开启线程需要占用一定的内存空间,默认情况下,每一个线程占用512KB- 如果开启
大量线程,会占用大量的内存空间,降低程序的性能 线程越多,CPU在调用线程上的开销就越大- 程序设计更加复杂,比如线程间的通信,多线程的数据共享
多线程的生命周期
苹果系统中的线程遵循经典的五态模型:新建 → 就绪 → 运行 → 阻塞 → 死亡 。
![[Pasted image 20260524101557.png]]
- 新建 :实例化线程对象(如
[[NSThread alloc] init...])。 - 就绪 :线程调用
start方法后被加入可调度线程池 ,等待 CPU 调度。注意调用start并不会立即执行。 - 运行:CPU 从可调度线程池中挑选线程执行,运行过程中可能在"就绪 ↔ 运行"之间反复切换,开发者无法干预。
- 阻塞 :满足某个条件时进入等待,常见手段包括
sleepUntilDate:、sleepForTimeInterval:以及@synchronized同步锁。被阻塞的线程在条件解除后重新回到就绪队列。 - 死亡 :分为正常死亡(任务执行完毕)和非正常死亡(调用
exit或cancel等强制终止)。其中exit会让后续代码完全不执行,而cancel只能取消还未运行的线程,无法中断正在执行的线程。
实现方案
苹果在不同抽象层提供了四套 API,从底层到上层依次是:
| 方案 | 语言/层级 | 线程管理 | 使用频率 | 特点 |
|---|---|---|---|---|
| pthread | C / 跨平台 POSIX 标准 | 程序员手动管理 | 几乎不用 | 最底层,可移植性强但使用繁琐 |
| NSThread | Objective-C 面向对象封装 | 程序员手动管理 | 偶尔使用 | 一个 NSThread 对象对应一条线程,可获取 currentThread |
| GCD(Grand Central Dispatch) | C 语言 + Block | 系统自动管理 | 最常用 | 充分利用多核,基于队列(串行/并发)调度任务 |
| NSOperation / NSOperationQueue | ||||
| 在后面主要会讲 GCD 相关的用法。 |
锁
说到多线程,必不可少的就是锁相关的内容。
锁主要有以下几类
- 互斥锁(Mutex)
- 递归锁(Recursive Lock)
- 信号量(Semaphore)
- 条件锁(Condition Lock)
- 读写锁(Read-Write Lock)
- 自旋锁(Spin Lock)
- 属性修饰符:atomic / nonatomic
- Once 原子操作
这边只是介绍一下锁的类型,因为锁是一个庞大的分类,所以我们下节博客会专门来去介绍锁相关的机制。
下面我们相关的内容全部是 GCD 的用法以及相关方法。
队列与函数
函数
同步执行
对应同步函数dispatch_sync
同步执行必须等待当前任务完成才能继续,且不会开启新线程。
异步执行
对应异步函数dispatch_async
异步执行无需等待,可继续往下走,并具备开启新线程的能力。
对列
串行队列和并发队列
多线程中所说的队列(Dispatch Queue)是指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,遵循先进先出(FIFO)原则,即新任务总是被插入到队尾,而任务的读取从队首开始读取。
在GCD中,队列主要分为串行队列 和并发队列两种,如图所示
![[Pasted image 20260524103339.png]]
串行队列:每次只有一个任务被执行,等待上一个任务执行完毕再执行下一个,即只开启一个线程- 我们用
dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);创建串行队列 - 这里面我们可以使用DISPATCH_QUEUE_SERIAL来代表串行,也可以直接使用 NULL表示,这是默认串行队列
- 我们用
cpp
// 串行队列的获取方法
dispatch_queue_t serialQueue1 = dispatch_queue_create("Queue", NULL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
并发队列:一次可以并发执行多个任务,即开启多个线程,并同时执行任务- 使用
dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);创建并发队列 - 使用并发队列一定要对应的使用异步函数这样才有用。
- 使用
cpp
// 并发队列的获取方法
dispatch_queue_t concurrentQueue = dispatch_queue_create("cocurrentQueue", DISPATCH_QUEUE_CONCURRENT);
在这里其实有两个比较特殊的队列一个是主队列一个是全局并发队列
- 主队列它其实本质上是一个串行队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();//获取串行队列我们可以这么创建。 - 全局并发队列,它本质上是一个并发队列
dispatch_queue_t uqe = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 获取全局并发队列我们可以这么创建。
在日常的开发中,他们是这么配合使用的
objc
//主队列 + 全局并发队列的日常使用
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//执行耗时操作
dispatch_async(dispatch_get_main_queue(), ^{
//回到主线程进行UI操作
});
});
GCD的基本使用
我们先说一下基础写法
cpp
//创建任务
dispatch_block_t block = ^{
NSLog(@"hello GCD");
};
//创建串行队列
dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", NULL);
//将任务添加到队列,并指定函数执行
dispatch_async(queue, block);
任务是需要放置到队列里的,我们可以指定函数。指令是异步的还是同步的。同时,我们也可以创建是否是串行队列,还是并行队列。
下面我们来探索一下任意不同的组合。
串行队列+同步函数
任务按顺序执行:任务一个接一个的在当前线程执行,不会开辟新线程
objc
- (void) test01 {
dispatch_queue_t serialQueue = dispatch_queue_create("LokyTestSerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"1 %d == %@", i, [NSThread currentThread]);
}
});
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"2 %d == %@", i, [NSThread currentThread]);
}
});
}

串行队列+异步函数
任务按顺序执行:任务一个接一个的执行,会开辟新线程
objc
- (void) test02 {
NSLog(@"串行+异步 %@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_queue_create("LokyTestSerialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"1 %d == %@", i, [NSThread currentThread]);
}
});
dispatch_async(serialQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"2 %d == %@", i, [NSThread currentThread]);
}
});
}

并发队列+同步函数
任务按顺序执行:任务一个接一个的执行,不开辟线程
objc
- (void) test03 {
NSLog(@"并发+同步 %@", [NSThread currentThread]);
dispatch_queue_t serialQueue = dispatch_queue_create("LokyTestSerialQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"1 %d == %@", i, [NSThread currentThread]);
}
});
dispatch_sync(serialQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"2 %d == %@", i, [NSThread currentThread]);
}
});
}

并发队列+异步函数
任务乱序执行:任务执行无顺序,会开辟新线程
objc
- (void) test04 {
NSLog(@"并发+异步 %@", [NSThread currentThread]);
dispatch_queue_t concurentQueue = dispatch_queue_create("LokyTestSerialQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurentQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"1 %d == %@", i, [NSThread currentThread]);
}
});
dispatch_async(concurentQueue, ^{
for (int i = 0; i < 3; i++) {
NSLog(@"2 %d == %@", i, [NSThread currentThread]);
}
});
}

主队列+同步函数
造成死锁:任务相互等待,造成死锁
objc
- (void) test05 {
NSLog(@"主+同步 %@", [NSThread currentThread]);
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"%@", [NSThread currentThread]);
});
}

造成死锁的原因分析如下:
-
主队列有两个任务,顺序为:
NSLog任务 - 同步block -
执行NSLog任务后,执行同步Block,会将任务1(即i=1时)加入到主队列,主队列顺序为:
NSLog任务 - 同步block - 任务1 -
任务1的执行需要等待同步block执行完毕才会执行,而同步block的执行需要等待任务1执行完毕,所以就造成了任务互相等待的情况,即造成死锁崩溃
死锁现象
-
主线程因为你同步函数的原因等着先执行任务 -
主队列等着主线程的任务执行完毕再执行自己的任务 -
主队列和主线程相互等待会造成死锁
剩下还有主 +异步函数, 全局并发+同步函数,全局并发+异步函数。这个和上面介绍的 4 个没有什么大的区别,唯一有区别的就是主加异步函数,它会指定线程为主线程。其他的与上面介绍的四个没有任何不同。
总结
| 函数\队列 | 串行队列 | 并发队列 | 主队列 | 全局并发队列 |
|---|---|---|---|---|
| 同步函数 | 顺序执行,不开辟线程 | 顺序执行,不开辟线程 | 死锁 | 顺序执行,不开辟线程 |
| 异步函数 | 顺序执行,开辟线程 | 乱序执行,开辟线程 | 顺序执行,不开辟线程 | 乱序执行,开辟线程 |
GCD 相关函数
dispatch_after
objc
- (void)testAfter{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"2s后输出");
});
}
这个的核心应用场景就是:在主队列上延迟执行一项任务,如viewDidload之后延迟1s,提示一个alertview(是延迟加入到队列,而不是延迟执行)。
这个输出我就不展示了,它就是单纯延迟了两秒后输出。
dispatch_once
objc
- (void)testOnce{
/*
dispatch_once保证在App运行期间,block中的代码只执行一次
应用场景:单例、method-Swizzling
*/
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//创建单例、method swizzled或其他任务
NSLog(@"创建单例");
});
}
这个就是创建单例。让 block 中的内容在全局代码中只执行一次,就比如我们可以通过它来维护一个变量。

dispatch_apply
objc
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"dispatch_apply 的线程 %zu - %@", index, [NSThread currentThread]);
});
我们先来介绍一下使用这个函数的三参数param1:重复次数 param2:追加的队列 param3:执行任务
下面我们给一个例子
objc
- (void)testApply{
dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_SERIAL);
NSLog(@"dispatch_apply前");
dispatch_apply(10, queue, ^(size_t index) {
NSLog(@"dispatch_apply 的线程 %zu - %@", index, [NSThread currentThread]);
});
NSLog(@"dispatch_apply后");
}

dispatch_apply将指定的Block追加到指定的队列中重复执行,并等到全部的处理执行结束------相当于线程安全的for循环。
应用场景:用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性。
dispatch_group_t
这个有两种使用方式
- 方式一:使用
dispatch_group_async + dispatch_group_notify
objc
- (void)testGroup1{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
NSLog(@"请求一完成");
});
dispatch_group_async(group, queue, ^{
NSLog(@"请求二完成");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新页面");
});
}

在这里如果要触发 notify 要把组里的跑完才能触发。
dispatch_group_t:调度组将任务分组执行,能监听任务组完成,并设置等待时间
应用场景:多个接口请求之后刷新页面
- 方式二:使用
dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
objc
- (void)testGroup2{
/*
dispatch_group_enter和dispatch_group_leave成对出现,使进出组的逻辑更加清晰
*/
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"请求一完成");
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(queue, ^{
NSLog(@"请求二完成");
dispatch_group_leave(group);
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新界面");
});
}

这个写法主要就是要让 enter 和 leave 成对出现。
dispatch_barrier_sync & dispatch_barrier_async
栅栏函数,主要有两种使用场景:串行队列、并发队列
我们先看串行队列
objc
- (void) testBarrier1{
//串行队列使用栅栏函数
dispatch_queue_t queue = dispatch_queue_create("Loky", DISPATCH_QUEUE_SERIAL);
NSLog(@"开始 - %@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"延迟2s的任务1 - %@", [NSThread currentThread]);
});
NSLog(@"第一次结束 - %@", [NSThread currentThread]);
//栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1、任务2
dispatch_barrier_async(queue, ^{
NSLog(@"------------栅栏任务------------%@", [NSThread currentThread]);
});
NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
dispatch_async(queue, ^{
sleep(2);
NSLog(@"延迟2s的任务2 - %@", [NSThread currentThread]);
});
NSLog(@"第二次结束 - %@", [NSThread currentThread]);
}
下面是输出结果

我们可以根据输出结果来看一下流程图
![[Pasted image 20260524135215.png]]
我们可以看到, 主线程先跑,在主线程跑完了以后,子线程跑自建队列里的任务。然后栅栏函数也发挥了自己分隔的作用。
下面我们看一个并发队列
objc
- (void)testBarrier3 {
dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
// 栅栏前:并发投 3 个任务
for (int i = 0; i < 3; i++) {
dispatch_async(queue, ^{
sleep(1);
NSLog(@"前-任务%d - %@", i, [NSThread currentThread]);
});
}
// 栅栏
dispatch_barrier_async(queue, ^{
NSLog(@"栅栏 - %@", [NSThread currentThread]);
});
// 栅栏后:并发投 3 个任务
for (int i = 0; i < 3; i++) {
dispatch_async(queue, ^{
sleep(1);
NSLog(@"后-任务%d - %@", i, [NSThread currentThread]);
});
}
}
下面是输出

在这里我们可以直接看出来,并发与串行的不同,我们可以直接看出来他执行任务的随机性,他并不会像串行一样按照顺序来去执行。同时,我们也可以发现里边的线程复用情况,在前边我们开启了三个线程来执行。在后面我们发现并不需要多开启线程来执行,所以说我们就复用了 234 这三条线程。
然后我们要知道栅栏的最大的作用就是实现同步锁,我们下边可以给一个类的例子。
objc
@implementation SafeCache {
NSMutableDictionary *_cache;
dispatch_queue_t _queue;
}
- (instancetype)init {
if (self = [super init]) {
_cache = [NSMutableDictionary dictionary];
// 关键 1:必须是自定义并发队列
_queue = dispatch_queue_create("safe.cache", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
// 读操作:并发同步执行
- (id)objectForKey:(NSString *)key {
__block id obj;
// dispatch_sync 同步等结果,不需要 barrier(读可并发)
dispatch_sync(_queue, ^{
obj = _cache[key];
});
return obj;
}
// 写操作:用栅栏独占执行
- (void)setObject:(id)obj forKey:(NSString *)key {
// dispatch_barrier_async 让写操作独占队列
dispatch_barrier_async(_queue, ^{
_cache[key] = obj;
});
}
- (void)removeObjectForKey:(NSString *)key {
dispatch_barrier_async(_queue, ^{
[_cache removeObjectForKey:key];
});
}
@end
在这里我们可以发现,读的操作是可以并发同步执行的,但是写的操作必须单线程,这样就是为了保证安全,防止竞争。
dispatch_semaphore_t
objc
- (void) testSemaphore{
dispatch_queue_t queue = dispatch_queue_create("Loky", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"当前 - %d, 线程 - %@", i, [NSThread currentThread]);
});
}
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
NSLog(@"当前 - %d, 线程 - %@", i, [NSThread currentThread]);
dispatch_semaphore_signal(sem);
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
}
}
下面是运行结果

前边那一部分是非常的乱就是随机的并发执行。但是从第二个 for 循环开始,我们通过信号量的增减。来控制线程数量来达到类似于同步执行的效果。
下边我给出来一张图

在这里我们可以发现,因为 for 循环是主线程里边的,然后跑到异步的时候。会直接把里边的 block 丢出去,然后执行下边的 wait,这个时候会发现,信号量居然变成负的了。因为执行 wait 会减 1。那么这个时候主线程就堵塞了,我们就会去跑子线程。跑完子线程以后,它里面有 signal 所以会让信号量加一,刚好为零。就会继续执行主线程。这样就会达到一个类似于同步的效果。
dispatch_source_t
这个主要就是实现计时的工作,类似于 nstimer, 但是精度比 nstimer 高。
objc
- (void)testSource{
//1.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//2.创建timer
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//3.设置timer首次执行时间,间隔,精确度
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0*NSEC_PER_SEC, 0.1*NSEC_PER_SEC);
//4.设置timer事件回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCDTimer");
});
//5.默认是挂起状态,需要手动激活
dispatch_resume(timer);
}
小结
这篇博客主要讲了 GCD 的基本使用和 GC 的相关函数,以及多线程是什么在下篇博客我们会介绍锁相关的内容。多线程和锁的关系是非常紧密的,学习了多线程,必须要学习多线程相关的锁。