多线程在 iOS 中有着举足轻重的地位,合适地使用多线程不仅可以保证App的质量,也提高用户的体验度
本文的目的在于了解进程、线程、多线程等基本概念及原理
同样的,几个问题:
- 线程是什么,进程是什么
- 线程与进程的关系
- 多线程原理
- 常用的多线程方案
线程和进程
线程
- 线程是进程的
基本执行单元,一个进程的所有任务都在线程中执行 - 进程要想执行任务,必须有线程,
进程至少要有一条线程 - 程序启动默认会开启一条线程,这条线程被称为
主线程或UI线程
进程
- 进程是指在系统中正在运行的一个应用程序
- 每个进程之间是相互独立的,每个进程均运行在其专用且受保护的内存空间
简单理解为
进程是线程的容器,而线程是用来执行任务
在 iOS 中是单进程开发,一个进程就是一个 App,进程之间是相互独立的,如支付宝、微信等,属于不同的一个进程
线程与进程的关系
进程与线程之间的关系,主要涉及两个方面:
- 地址空间
- 同一个进程的线程共享本进程的地址空间
- 进程之间则是独立的地址空间
- 资源拥有
- 同一个进程内线程共享本进程的资源,如内存、I/O、CPU 等
- 进程之间资源是相互独立的
两者的关系就如同工厂与流水线的关系,工厂与工厂之间是独立的,但工厂中的流水线是共享工厂的资源
除此之外,还有几点:
- 多进程要比多线程健壮
- 一个进程崩溃后,在保护模式下不会对其他进程产生影响
- 一个线程崩溃后,整个进程都会死掉
- 使用场景:频繁切换、并发操作
- 进程切换时,消耗资源大,效率高。所以涉及到频繁切换时,使用线程要好于进程
- 如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
- 执行过程
- 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口
- 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
线程是处理器的基本单元,但是进程不是- 线程没有地址空间,线程包含在进程地址空间中
线程与进程的关系图

线程和RunLoop的关系
runloop 与线程是一一对应的,一个 runloop 对应一个核心的线程
因为 runloop 是可以嵌套的,但是核心的只能有一个,它们的关系保存在一个全局的字典里
runloop 是来管理线程的,当线程的 runloop 被开启后,线程会在执行完任务后进入休眠状态,有任务就会被唤醒去执行任务
runloop 在第一次获取时被创建,在线程结束时被消耗
- 对于主线程来说,runloop 在程序启动时就默认创建好了
- 对于子线程来说,runloop 是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的 runloop 被创建
多线程
多线程原理
对于单核 CPU,同一时间,CPU只能处理一条线程,即只有一条线程在工作
多线程并发执行,其实是 CPU 在多条线程之间快速地调度
多线程意义
- 优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率,如 CPU、内存
- 线程上的任务执行完成后,线程会自动销毁
- 缺点
- 开启线程需要占用一定的内存空间,默认情况下,每一条线程占用 512KB,创建线程大约需要 90ms 的创建时间
- 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调用线程上的开销越大
- 程序设计更加复杂,比如线程间的通信,多线程的数据共享
多线程生命周期
多线程的生命周期主要分为 5 部分:新建-就绪-运行-堵塞-死亡

- 新建:主要是实例化线程对象
- 就绪:线程对象调用 start 方法,将线程对象加入可调度线程池,等等 CPU 的调用
- 运行:CPU 负责调度可调度线程池中线程的执行。在线程执行完成任务之前,状态可能会在就绪和运行之间来回切换,这个变化是由 CPU 负责的,程序员不能干预
- 阻塞:当满足某个预定条件时,可以使用休眠——sleep或同步锁,阻塞线程执行,当进入 sleep时,会重新将线程加入就绪状态
sleepUntilDate——阻塞当前线程,直到指定的时间为止,即休眠到指定时间sleepForTimeInterval——在给定的时间间隔内休眠线程,即指定休眠时长@synchronized()——同步锁
- 死亡:分为两种情况
- 正常死亡,线程执行完毕,自动销毁
- 非正常死亡,在满足某个条件后,在线程内部终止执行/在主线程中止线程对象
线程的 exit 和 cancel 说明
exit——一旦强行终止线程,后续的所有代码都不会执行cancel——取消当前线程,但是不能取消正在执行的线程
线程的优先级越高,是不是意味着任务的执行越快?
答:并不是,影响线程执行速度的因素有以下几点:
- CPU 调度情况 - 任务的复杂度 - 任务的优先级 - 任务队列的情况
线程池原理
若
线程池大小小于核心线程池时,会直接去创建线程执行任务若
线程池大小大于等于核心线程池时,则按下图的流程
- 判断核心线程池是否都正在执行任务
- 如果没有,那么会创建新的线程去执行任务
- 判断线程池的工作队列是否已经饱满
- 如果没有,会将任务加入到工作队列中,等待 CPU 调度
- 判断线程池中的线程是否都处于执行状态
- 如果没有,安排可调度线程池中空闲的线程去执行任务
- 交给饱和策略去执行,主要有四种
AbortPolicy:直接抛出RejectedExecutionExeception异常来阻止系统正常运行CallerRunsPolicy:将任务回退到调用者DisOldestPolicy:丢掉等待最久的任务DisCardPolicy:直接丢弃任务
- 判断核心线程池是否都正在执行任务
iOS中多线程的实现方案
iOS 中多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation

简单实例:
// *********1: pthread*********
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
/**
pthread_create 创建线程
参数:
1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
同时不需要 `*`
2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
3. 线程要执行的`函数地址`
void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*): 函数名
(void *): 参数类型,void *
4. 传递给第三个参数(函数)的`参数`
*/
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
NSLog(@"成功");
} else {
NSLog(@"失败");
}
void *pthreadTest(void *para){
// 接 C 语言的字符串
// NSLog(@"===> %@ %s", [NSThread currentThread], para);
// __bridge 将 C 语言的类型桥接到 OC 的类型
NSString *name = (__bridge NSString *)(para);
NSLog(@"===>%@ %@", [NSThread currentThread], name);
return NULL;
}
//*********2、NSThread*********
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
//*********3、GCD*********
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
//*********4、NSOperation*********
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];
- (void)threadTest{
NSLog(@"begin");
NSInteger count = 1000 * 100;
for (NSInteger i = 0; i < count; i++) {
// 栈区
NSInteger num = i;
// 常量区
NSString *name = @"zhang";
// 堆区
NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
NSLog(@"%@", myName);
}
NSLog(@"over");
}
C 和 OC 的桥接
__bridge只做类型转换,但是不修改对象(内存)管理权__bridge_retained(也可以使用CFBridgingRetain)将Objective-C的对象转换为Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象__bridge_transfer(也可以使用CFBridgingRelease)将Core Foundation的对象 转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC
线程安全问题
当多线程同时访问一块资源时,容易引发数据错乱和数据安全的问题,常见的解决方案是通过锁
关于锁的分析,可以查看iOS底层原理探索-常见的锁分析
线程间通信
在Threading Programming Guide文档中,提及,线程间的通讯有以下几种方式

直接消息传递: 通过performSelector的一系列方法,可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化全局变量、共享内存块和对象: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性。 否则可能会导致竞争状况,数据损坏或崩溃。条件执行: 条件是一种同步工具,可用于控制线程何时执行代码的特定部分。您可以将条件视为关守,让线程仅在满足指定条件时运行。Runloop sources: 一个自定义的 Runloop source 配置可以让一个线程上收到特定的应用程序消息。由于Runloop source 是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率Ports and sockets:基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用 Runloop source 来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态。需要注意的是,端口通讯需要将端口加入到主线程的Runloop中,否则不会走到端口回调方法消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效Cocoa 分布式对象: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高
总结
本文主要对线程、进程、多线程的基本概念与原理,简单的了解
关于 GCD 的使用及底层原理会在后续进行详细分析