基础
进程
指在系统中正在运行的一个应用程序
每个进程之间是相互独立的,每个进程运行在其专用的并且受保护的内存空间内
线程
一个进程想要执行任务,必须要有线程,每个进程至少有一个线程
一个进程的所有任务都是在线程中执行的
线程的串行
一个线程中任务的执行是串行的,如果要在一个线程中执行多个任务,那么只能一个一个按顺序地执行这些任务,同一时间内,一个线程只能执行一个任务
多线程
一个进程中可以开启多个线程,每个线程可以并行(同时)执行不同的任务
原理
同一时间,CPU只能处理一个线程,只有一条线程在工作(执行)
多线程并发(同时)执行,其实是CPU快速地在多个线程之间进行调度(切换)
如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
优点
适当提高程序执行效率,适当提高资源(CPU、内存)利用率
缺陷
如果线程过多,CPU在太多线程之间调度,会消耗大量的CPU资源。
开过多线程会降低程序性能。
创建线程需要开销,iOS下主要成本包括:内核数据结构(大约1KB),栈空间(子线程512KB,主线程1MB,也可以用-setSize:设置,但是大小必须是4K的倍数并且最小16K),创建线程大约需要90毫秒的时间。
程序设计更复杂,比如线程间通信、多线程数据共享
线程生命周期
进入就绪状态会加入 内存的可调度线程池中,一旦线程死亡,就不能再开启任务,需要重新创建
多线程的安全隐患
资源共享
一块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源,比如多个线程访问同一个对象、同一个变量、同一个文件。
互斥锁
使用格式
@synchronized (锁对象) {需要锁定的代码}优缺点
优点:能有效防止因多线程抢夺资源造成的数据安全问题
缺点:消耗大量的CPU资源使用前提
多条线程存在抢夺同一块资源线程同步
多条线程在同一条线上执行(按顺序地执行任务)
互斥锁就是使用了线程同步技术
原子核非原子属性
OC 在定义时有nonatomic和atomic两种选择
atomic
:原子属性,为setter方法加锁(默认就是atomic)
nonatomic
:非原子属性,不会为sette方法加锁
线程间通信
概念
在一个进程中,线程往往不是孤独存在的,多个线程之间需要经常进行通信
线程间通信的体现
一个线程传递数据给另一个线程
在一个线程中执行完特定任务后,转到另一个线程继续执行任务
例如:子线程下载,返回主线程刷新
线程间通信方法
NSPort、NSMessagePort、NSMachPort
主线程和子线程之间通过Port端口来进行数据传递
事件处理
响应者链条
Runtime
Runloop
多线程在iOS开发中的应用
主线程
一个iOS程序运行后,默认会开启一条线程,称为主线程或UI线程
主线程作用
显示\刷新UI界面
处理UI事件(比如点击事件、滚动事件、拖拽事件等)
主线程使用注意
比较耗时的操作不应该放到主线程中(下载等)
还是操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
iOS多线程分类
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 一套通用的多线程API 适用于Unix\Linux\Windows等系统 跨平台\可移植 | C | chengxuyan | 几乎不用 |
NSThread | 使用更加面向对象 简单易用,可直接操作线程对象 | OC | 程序员管理创建 | 偶尔 |
GCD | 旨在替代NSThread等线程技术 充分利用设备的多核 | C | 自动管理 | 经常使用 |
NSOperation | 底层是GCD 比GCD多了一些更简单使用的功能 使用更加面向对象 | OC | 自动管理 | 经常使用 |
概述
在 iOS 中其实目前有 4 套多线程方案,他们分别是:
- Pthreads
- NSThread
- GCD
- NSOperation & NSOperationQueue
Pthreads
导头文件
#import
创建线程,并执行任务
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
pthread_t thread;
//创建一个线程并自动执行
pthread_create(&thread, NULL, start, NULL);
}
void *start(void *data) {
NSLog(@"%@", [NSThread currentThread]);
return NULL;
}
NSThread
这套方案是经过苹果封装后的,并且完全面向对象的。所以你可以直接操控线程对象,非常直观和方便。但是,它的生命周期还是需要我们手动管理,所以这套方案也是偶尔用用,比如 [NSThread currentThread],它可以获取当前线程类,你就可以知道当前线程的各种属性,用于调试十分方便。下面来看看它的一些用法。
先创建线程类,再启动
// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
// 启动
[thread start];
创建并自动启动
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];
使用 NSObject 的方法创建并自动启动
[self performSelectorInBackground:@selector(run:) withObject:nil];
其他方法
//取消线程
- (void)cancel;
//启动线程
- (void)start;
//判断某个线程的状态的属性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
//设置和获取线程名字
-(void)setName:(NSString *)n;
-(NSString *)name;
//获取当前线程信息
+ (NSThread *)currentThread;
//获取主线程信息
+ (NSThread *)mainThread;
//使当前线程暂停一段时间,或者暂停到某个时刻
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;
GCD
Grand Central Dispatch。它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。同时它使用的也是 c语言,不过由于使用了 Block(Swift里叫做闭包),使得使用起来更加方便,而且灵活。所以基本上大家都使用 GCD 这套方案。
任务和队列
任务
:即操作,你想要干什么,说白了就是一段代码,在 GCD 中就是一个 Block,所以添加任务十分方便。任务有两种执行方式:同步(sync) 和 异步(async) 的主要区别在于会不会阻塞当前线程,直到 Block 中的任务执行完毕。
- 如果是 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。
- 如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。
队列
:用于存放任务。一共有两种队列, 串行队列 和 并行队列
放到串行队列的任务,GCD 会 FIFO(先进先出) 地取出来一个,执行一个,然后取下一个,这样一个一个的执行。
执行方式 | 并行队列 | 串行队列 | 主队列 |
---|---|---|---|
同步(sync) | 没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
没有开启新线程 串行执行任务(可能死锁) |
异步(async) | 开启新线程 并行执行任务 |
开启新线程 串行执行任务 |
没有开启新线程 串行执行任务 |
同步:不开启新的线程
异步:可以开启新的线程
并行:可以多任务同时执行
串行:顺序执行任务
创建队列
主队列
:这是一个特殊的串行队列,它用于刷新 UI,任何需要刷新 UI 的工作都要在主队列执行,所以一般耗时的任务都要放到别的线程执行。
dispatch_queue_t queue = dispatch_get_main_queue()
全局并行队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
创建任务
同步任务
:不会另开线程 (SYNC)
dispatch_sync(, ^{
//code here
NSLog(@"%@", [NSThread currentThread]);
});
异步任务
:会另开线程 (ASYNC)
dispatch_async(, ^{
//code here
NSLog(@"%@", [NSThread currentThread]);
});
**死锁**
同步任务会阻塞当前线程,然后把 Block 中的任务放到指定的队列中执行,只有等到 Block 中的任务完成后才会让当前线程继续往下运行。
那么这里的步骤就是:打印完第一句后,dispatch_sync 立即阻塞当前的主线程,然后把 Block 中的任务放到 main_queue 中,可以 main_queue 中的任务会被取出来放到主线程中执行,但主线程这个时候已经被阻塞了,所以 Block 中的任务就不能完成,它不完成,dispatch_sync 就会一直阻塞主线程,这就是死锁现象。导致主线程一直卡死。
NSLog("之前 - %@", NSThread.currentThread())
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
NSLog("sync - %@", NSThread.currentThread())
})
NSLog("之后 - %@", NSThread.currentThread())
队列组
队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组里所有的任务都执行完了,队列组会通过一个方法通知我们。下面是使用方法,这是一个很实用的功能。
//1.创建队列组
dispatch_group_t group = dispatch_group_create();
//2.创建队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//3.多次使用队列组的方法执行任务, 只有异步方法
//3.1.执行3次循环
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i < 3; i++) {
NSLog(@"group-01 - %@", [NSThread currentThread]);
}
});
//3.2.主队列执行8次循环
dispatch_group_async(group, dispatch_get_main_queue(), ^{
for (NSInteger i = 0; i < 8; i++) {
NSLog(@"group-02 - %@", [NSThread currentThread]);
}
});
//3.3.执行5次循环
dispatch_group_async(group, queue, ^{
for (NSInteger i = 0; i < 5; i++) {
NSLog(@"group-03 - %@", [NSThread currentThread]);
}
});
//4.都完成后会自动通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"完成 - %@", [NSThread currentThread]);
});
NSOperation和NSOperationQueue
NSOperation 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。 大家可以看到 NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。操作步骤也很好理解:
- 将要执行的任务封装到一个 NSOperation 对象中。
- 将此任务添加到一个 NSOperationQueue 对象中。
添加任务
值得说明的是,NSOperation 只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务。分别是:NSInvocationOperation
和 NSBlockOperation
。创建一个 Operation 后,需要调用 start 方法来启动任务,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel 方法即可。
NSInvocationOperation
: 需要传入一个方法名
//1.创建NSInvocationOperation对象
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
//2.开始执行
[operation start];
NSBlockOperation
//1.创建NSBlockOperation对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//2.开始任务
[operation start];
之前说过这样的任务,默认会在当前线程执行。但是 NSBlockOperation 还有一个方法:addExecutionBlock:
,通过这个方法可以给 Operation 添加多个执行 Block。这样 Operation 中的任务 会并发执行,它会 在主线程和其它的多个线程 执行这些任务,注意下面的打印结果
//1.创建NSBlockOperation对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//添加多个Block
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
//2.开始任务
[operation start];
注意:
addExecutionBlock 方法必须在 start() 方法之前执行,否则就会报错:
自定义Operation
除了上面的两种 Operation 以外,我们还可以自定义 Operation。自定义 Operation 需要继承 NSOperation 类,并实现其 main() 方法,因为在调用 start() 方法的时候,内部会调用 main() 方法完成相关逻辑。所以如果以上的两个类无法满足你的欲望的时候,你就需要自定义了。你想要实现什么功能都可以写在里面。除此之外,你还需要实现 cancel() 在内的各种方法。
创建队列
每套多线程方案都会有一个主线程(当然啦,说的是iOS中,像 pthread 这种多系统的方案并没有,因为 UI线程 理论需要每种操作系统自己定制)。这是一个特殊的线程,必须串行。所以添加到主队列的任务都会一个接一个地排着队在主线程处理。
主队列
NSOperationQueue *queue = [NSOperationQueue mainQueue];
其他队列
//1.创建一个其他队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//2.创建NSBlockOperation对象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
//3.添加多个Block
for (NSInteger i = 0; i < 5; i++) {
[operation addExecutionBlock:^{
NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
}];
}
//4.队列添加任务
[queue addOperation:operation];
添加依赖
//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二
//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
注意
- 不能添加相互依赖,会死锁,比如 A依赖B,B依赖A。
- 可以使用 removeDependency 来解除依赖关系。
- 可以在不同的队列之间依赖,反正就是这个依赖是添加到任务身上的,和队列没关系。
NSOperation
BOOL executing; //判断任务是否正在执行
BOOL finished; //判断任务是否完成
void (^completionBlock)(void); //用来设置完成后需要执行的操作
- (void)cancel; //取消任务
- (void)waitUntilFinished; //阻塞当前线程直到此任务执行完毕
NSOperationQueue
NSUInteger operationCount; //获取队列的任务数
- (void)cancelAllOperations; //取消队列中所有的任务
- (void)waitUntilAllOperationsAreFinished; //阻塞当前线程直到此队列中的所有任务执行完毕
[queue setSuspended:YES]; // 暂停queue
[queue setSuspended:NO]; // 继续queue
常用方法
NSThread
创建
NSThread * thread = [[NSThread alloc]initWithTarget:self selector:@selector(downloadTask) object:nil];
//设置线程名
thread.name = @"aaa";
启动
[thread start];
快捷创建并执行
[NSThread detachNewThreadSelector:@selector(downloadTask) toTarget:self withObject:nil];
[self performSelectorInBackground:@selector(downloadTask) withObject:nil];
//回主线程执行方法,Object传参,UntilDone表示是否等待主线程完成任务之后才继续执行后面的程序
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
睡眠/阻塞线程
//睡指定时间
[NSThread sleepForTimeInterval:10];
//睡到某个时间之后
NSDate * date = [NSDate dateWithTimeIntervalSinceNow:10];
[NSThread sleepUntilDate:date];
强制停止线程
[NSThread exit];
GCD
创建线程
//同步线程(只能在当前线程中执行任务,不能开启子线程)
dispatch_sync(dispatch_queue_t queue, ^(void)block)
//异步线程
dispatch_async(dispatch_queue_t queue, ^(void)block)
创建队列
//创建一个并行队列 label:相当于队列名字 attr:队列的类型(DISPATCH_QUEUE_SERIAL串行,DISPATCH_QUEUE_CONCURRENT并行队列)
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);