多线程是iOS开发中的一个重要的环节,不管是在日常的开发,还是面试中,都有着举足轻重的地位,所以打算开辟一个小专题,研究多线程相关的底层原理。这一篇章是第一篇,介绍一些基础的概念性的多线程。
进程
定义
当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存。
在iOS
开发中,一个App在内存中就是一个进程,且相互独立,只能访问自己的沙盒控件,这也是苹果运行能够流畅安全的一个主要原因。
特点
- 独立性:是系统独立存在的实体,拥有自己独立的资源,有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户的进程不可以直接访问其他进程的地址空间。
- 动态性:进程与程序的区别在于:程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集和,进程中加入了时间的概念。进程具有自己的生命周期和不同的状态,这些都是程序不具备的。
- 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响。
线程
定义
线程的定义,主要可以归结为以下3点:
- 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
- 进程要想执行任务,必须得有线程,进程至少要有一条线程
- 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程
进程与线程的关系
-
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
-
资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间 的资源是独立的。
-
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程 都死掉。所以多进程要比多线程健壮。
-
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。 同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
-
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线 程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
线程是处理器调度的基本单位,但是进程不是。
线程与Runloop的关系
runloop
与线程是一一对应的,一个runloop
对应一个核心的线程,为什么说是核心的, 是因为runloop
是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。runloop
是来管理线程的,当线程的runloop
被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。runloop
在第一次获取时被创建,在线程结束时被销毁。- 对于主线程来说,
runloop
在程序一启动就默认创建好了。 - 对于子线程来说,
runloop
是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop
被创建,不然定时器不会回调
队列
定义
队列,又称为伫列(queue
),是先进先出(FIFO, First-In-First-Out
)的线性表,在具体应用中通常用链表或者数组来实现。装载线程任务的队形结构。队列只允许在后端(称为rear
)进行插入操作,在前端(称为front
)进行删除操作。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。

类型
队列的类型决定了任务的执行方式(并发、串行),队列包括以下几种:
- 并发队列(
Concurrent Dispatch Queue
): 线程执行可以同时一起进行执行,不需要上一个执行完,就能执行下一个的。 - 串行队列(
Serial Dispatch Queue
): 线程执行只能依次逐一先后有序的执行,等待上一个执行完,再执行下一个。 - 主队列:绑定主线程,所有任务都在主线程中执行,有经过特殊处理的串行队列。
- 全局队列:系统提供的并发队列。
同步、异步
同步 sync
: 只能在当前线程按先后顺序依次执行任务,不具备开启新线程的能力。
异步 async
: 在新的线程中执行任务,具备开启新线程的能力。

多线程
概念和原理
一个进程中可以并发多个线程同时执行各自的任务,叫做多线程。
分时操作系统会把CPU
的时间划分为长短基本相同的时间区间,叫时间片,在一个时间片内,CPU
只能处理一个线程中的一个任务,对于一个单核CPU来说,在不同的时间片来执行不同线程中的任务,就形成了多个任务在同时执行的“假象”。
多线程即为单位时间片里快速在各个线程之间切换
生命周期
- 新建(New): 新建线程
- 就绪(Runnable) :
start
之后就会runnable
,然后等待cpu
分配资源,进行调用 - 运行(Running) : 当获得
cpu
调度之后就会到running
状态 - 阻塞(Block) : 当线程中任务异常是,比如
sleep
,或者死锁等操作之后就会造成线程阻塞.当阻塞解除之后不会直接到running
状态而是又到就绪状态 - 死亡(Dead) : 任务执行完成或者强制退出

优缺点
优点:
- 能适当提高程序的执行效率
- 能适当提高资源利用率(cpu,内存)
- 线程上的任务执行完成后,线程会自动销毁
缺点:
- 开启线程需要占用一定的内存空间(默认情况下,每个线程都占512KB)
- 如果开启大量的线程,会占用大量的内存空间,降低程序性能
- 线程越多,CPU在调用线程上的开销越大
- 程序设计更加复杂,比如线程间的通讯,多线程的数据共享
iOS中的多线程方案
pthread
:即POSIX Thread
,缩写称为pthread
,是线程的POSIX
标准,是一套通用的多线程API
,可以在Unix/Linux/Windows
等平台跨平台使用。iOS中基本不使用。NSThread
:苹果封装的面向对象的线程类,可以直接操作线程,比起GCD
,NSThread
效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。GCD
:全称Grand Central Dispatch
,由C语言实现,是苹果为多核的并行运算提出的解决方案,CGD
会自动利用更多的CPU
内核,自动管理线程的生命周期,程序员只需要告诉GCD
需要执行的任务,无需编写任何管理线程的代码。GCD
也是iOS使用频率最高的多线程技术。NSOperation
:基于GCD
封装的面向对象的多线程技术,常配合NSOperationQueue
使用,使用频率较高。
GCD和NSOperation区别
GCD
仅仅支持FIFO
队列,不支持异步操作之间的依赖关系设置。而NSOperation
中的队列可以被重新设置优先级,从而实现不同操作的执行顺序调整。NSOperation
支持KVO
,可以观察任务的执行状态。GCD
更接近底层,GCD
在追求性能的底层操作来说,是速度最快的。- 从异步操作之间的事务性,顺序行,依赖关系。
GCD
需要自己写更多的代码来实现,而NSOperation
已经内建了这些支持 - 如果异步操作的过程需要更多的被交互和UI呈现出来,
NSOperation
更好。底层代码中,任务之间不太互相依赖,而需要更高的并发能力,GCD
则更有优势
小结图表

线程池
定义
线程池是多线程处理的一种形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池中的线程都是后台线程。每个线程都有默认的堆栈大小,以默认的优先级运行,并处在多线程单元中。
执行流程
- 判断线程池大小是否小核心线程池大小
- 如果小于.创建线程执行任务
- 如果不小于 判断工作队列是否已满,不满,将任务提交到工作队列,创建线程执行任务,如果已满,判断线程是否都在工作,如果都在工作交给饱和策略,如果没满创建线程执行任务

饱和策略
AbortPolicy
:默认策略。直接抛出RejectedExecutionExeception
异常阻止系统正常运行,该异常由调用者捕获CallerRunsPolicy
:调节机制。既不抛弃也不报异常。将任务回退给调用者DisOldestPolicy
:丢掉等待最久的任务DisCardPolicy
:直接丢弃任务
线程间通讯
苹果的官方文档中,给出了几种线程间通信的方式:

- 直接消息传递: 通过
performSelector
的一系列方法,可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化。 - 全局变量、共享内存块和对象: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性。 否则可能会导致竞争状况,数据损坏或崩溃。
- 条件执行: 条件是一种同步工具,可用于控制线程何时执行代码的特定部分。您可以将条件视为关守,让线程仅在满足指定条件时运行。
Runloop sources
: 一个自定义的Runloop source
配置可以让一个线程上收到特定的应用程序消息。由于Runloop source
是事件驱动的,因此在无事可做时,线程会自动进入睡眠状态,从而提高了线程的效率。Ports and sockets
: 基于端口的通信是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用Runloop source
来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态。- 消息队列: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效。
Cocoa
分布式对象: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高
端口通信例子
对于performSelector
的通信,平时在开发中运用的比较多,就不详细讲述了,我们讲一个线程之前,运用端口来传递消息的例子来加深印象。
首先我们创建一个类来发送消息:
@interface WYPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end
#import "WYPerson.h"
@interface WYPerson()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end
@implementation WYPerson
- (void)personLaunchThreadWithPort:(NSPort *)port{
NSLog(@"VC 响应了Person里面");
@autoreleasepool {
✅//1. 保存主线程传入的port
self.vcPort = port;
✅//2. 设置子线程名字
[[NSThread currentThread] setName:@"WYPersonThread"];
❗️//3. 开启runloop(重点在这)
[[NSRunLoop currentRunLoop] run];
✅//4. 创建自己port
self.myPort = [NSMachPort port];
✅//5. 设置port的代理回调对象(NSMachPortDelegate)
self.myPort.delegate = self;
✅//6. 完成向主线程port发送消息
[self sendPortMessage];
}
}
/**
* 完成向主线程发送port消息
*/
- (void)sendPortMessage {
NSData *data1 = [@"WY" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
✅// 发送消息到VC的主线程
✅// 第一个参数:发送时间。
✅// msgid 消息标识。
✅// components,发送消息附带参数。
✅// reserved:为头部预留的字节数
[self.vcPort sendBeforeDate:[NSDate date]
msgid:10086
components:array
from:self.myPort
reserved:0];
}
#pragma mark - NSMachPortDelegate 处理端口传递信息
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"person:handlePortMessage == %@",[NSThread currentThread]);
NSLog(@"从VC 传过来一些信息:");
NSLog(@"components == %@",[message valueForKey:@"components"]);
NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
}
接着我们创建PortViewController
用来接收消息和回调消息给发送者
#import "PortViewController.h"
#import <objc/runtime.h>
#import "WYPerson.h"
@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) WYPerson *person;
@end
@implementation PortViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Port线程通讯";
self.view.backgroundColor = [UIColor whiteColor];
✅//1. 创建主线程的port
✅// 子线程通过此端口发送消息给主线程
self.myPort = [NSMachPort port];
✅//2. 设置port的代理回调对象
self.myPort.delegate = self;
❗️//3. 把port加入runloop,接收port消息(此时主线程已经再跑,不需要run)
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.person = [[WYPerson alloc] init];
[NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
toTarget:self.person
withObject:self.myPort];
}
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"VC == %@",[NSThread currentThread]);
NSLog(@"从person 传过来一些信息:");
//会报错,没有这个隐藏属性
//NSLog(@"from == %@",[message valueForKey:@"from"]);
NSArray *messageArr = [message valueForKey:@"components"];
NSString *dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"传过来一些信息 :%@",dataStr);
NSPort *destinPort = [message valueForKey:@"remotePort"];
if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
NSLog(@"传过来的数据有误");
return;
}
NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
❗️❗️❗️// 非常重要,如果你想在Person的port接受信息,必须加入到当前主线程的runloop
[[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
NSLog(@"VC == %@",[NSThread currentThread]);
BOOL success = [destinPort sendBeforeDate:[NSDate date]
msgid:10010
components:array
from:self.myPort
reserved:0];
NSLog(@"%d",success);
}
- (void)getAllProperties:(id)somebody{
u_int count = 0;
objc_property_t *properties = class_copyPropertyList([somebody class], &count);
for (int i = 0; i < count; i++) {
const char *propertyName = property_getName(properties[i]);
NSLog(@"%@",[NSString stringWithUTF8String:propertyName]);
}
}
打印结果如下:

通过上面的例子,我们已经实现了线程之间的通信
稍微总结一下NSPort
的使用要点:
NSPort
对象必须添加到要接收消息的线程的Runloop
中,必须由Runloop
来进行管理- 接收消息的对象实现
NSPortDelegate
协议的-handlePortMessage:
方法来获取消息内容