多线程详解
多线程基础相关知识
进程:类似车间
系统正在运行的应用程序;每个进程都是独立的,都运行在其专用且受保护的内存空间中
每个进程可以开启多条线程
线程:类似车间里面的工人
一个iOS程序启动后会默认开启一条线程,即主线程/UI线程
是进程的执行单元,一个进程要想执行任务,只要要有一条线程;进程只会分配内存,并不会执行任务,所以执行任务必须要至少有一条线程。
例如:网易云音乐进程,那么播放音乐任务就要开启线程来执行了
一条线程中的的任务执行是串行的,也就是说在同一时间内只能执行一个任务;线程是进程中的一条执行路径
线程的状态
- 1、新建状态(New)
- 当线程被创建出来的时候就处于这种状态,此时会给线程对象分配内存空间,但线程此时并不能被执行
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(download) object:nil];
- 2、就绪状态(Runnalbe)
- 当线程调用
start
方法后,线程才具备可被调度的能力,此时线程处于就绪状态
,并把线程对象放到可调度线程池
,可调度线程池
里面还有其他的线程对象
- 当线程调用
[thread start];//线程此时具有可被调度的能力
- 3、运行状态(Running)
- 当CPU调度到当前线程的时候,此时线程是处于
运行状态
,如果当CPU调度可调度线程池里面的其他线程对象的时候,该线程又会进入就绪状态,所以线程对象是不断的在这两种状态中切换
- 当CPU调度到当前线程的时候,此时线程是处于
- 4、阻塞状态(Blocked)
- 如果线程调用了sleep方法/等待同步锁,线程就会进入阻塞状态,因此线程将失去可被调度的能力,此时线程对象会从可调度线程池中移除,但还存在与内存中
//睡10秒
[NSThread sleepForTimeInterval:10];
//从现在起睡3秒
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
- 5、如果sleep方法到时了/得到同步锁时,线程会从中“醒来”,处于就绪状态,因此再次放进可调度线程池里面
- 6、死亡状态(Dead)
- 如果线程在中途崩溃了,或者执行完任务,此时线程对象就会从内存中移除
- 已经死亡的线程无法再次启动
多线程的原理
同一时间,CPU只能处理一条线程,但是如果如果CPU能快速在不同的线程之间进行调度(切换),就会造成多条线程同时执行的假象。
一个App运行之后,系统会默认开启一条线程,也就是“主线程”,也称为“UI线程”;
注意点:不要把耗时的任务放到主线程中,不然会影响UI的刷新,导致卡顿的现象,应该把耗时的任务放到子线程中
多线程的几种实现方案
- 在iOS中,最常用的多线程实现方案是GCD和NSOperation,NSThread偶尔使用,pthread仅作了解即可
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 一套通用的多线程API;适用于Unix\Linux\Windows等系统;跨平台\可移植;使用难度大 | C | 程序员管理 | 几乎不用 |
NSThread | 使用更加面向对象;简单易用,可以直接操作线程对象 | OC | 程序员管理 | 偶尔使用 |
GCD | 旨在替代NSThread等线程技术;充分利用设备的多核 | C | 自动管理 | 经常使用 |
NSOpreation | 基于GCD(底层是GCD);比GCD多了一些更简单使用的功能;使用更加面向对象 | OC | 自动管理 | 经常使用 |
NSThread
//第一种创建方式(显式创建并手动启动):
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(download) object:nil];
//启动
[thread1 start];
//第二种创建方式(显式创建后自动启动,但无法对线程进行详细的设置,因为无法取得线程对象):
[NSThread detachNewThreadSelector:@selector(download:) toTarget:self withObject:@"www.baidu.com"];
//第三种创建方式(隐式创建并自动启动,但无法对线程进行详细的设置,因为无法取得线程对象):
[self performSelectorInBackground:@selector(download:) withObject:@"www.google.com.cn"];
//判断是否为主线程
+ (NSThread *)mainThread;//获得主线程
- (BOOL)isMainThread;//是否为主线程
+ (BOOL)isMainThread;//是否为主线程
//判断正在执行的方法是否在主线程中执行
[NSThread isMainThread];
GCD
-
优势
- GCD是苹果公司为
多核
的并行
运算提出的解决方案 - GCD会自动利用更多的CPU内核(比如双核、四核)
- GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- GCD是苹果公司为
-
两个核心概念:
- 任务: 执行什么操作,即想要做什么
- 队列:用来存放任务(并发,串行队列)
-
执行步骤:
- 1、确定要做的事情(制定任务)
- 2、将任务添加到队列中
1、GCD会
自动
将队列中的任务取出,放到对应的线程中执行
2、无论是并发还是串行队列,里面的任务都遵循FIFO
原则:先进先出,只不过在并发队列中,它会取出一个任务放到子线程中执行,再取出一个放到另外的子线程中执行,由于去任务的速度快,并且又是多条线程执行,所以看起来所有任务是一起并发执行;但是并发的数量由系统决定,所以当任务很多的时候,并不会让所有任务一起执行。 主要执行任务的函数
//同步函数:不具备开启线程的能力,会阻塞当前线程,直到Block中的任务执行完毕,然后再执行当前线程的任务。
dispatch_sync(dispatch_queue_t queue, ^(void)block)
//异步函数:具备开启线程的能力,不会阻塞当前线程,即不用等待Block执行完毕才执行当前方法,当前方法执行完毕再回来执行Block里面的代码。
dispatch_async(dispatch_queue_t queue, ^(void)block)
队列:决定了任务的执行方式
-
类型
- 1、并发队列:可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步函数下才有效
- 2、串行队列:让任务一个接一个地执行(一个任务执行完毕后,再执行下一个任务)
- 串行队列有:主队列;自己创建的串行队列
- 并发队列有:全局并发队列;自己创建的并发队列
全局并发队列
//两个参数填0即可
dispatch_queue_t queue = dispatch_get_global_queue(long identifier, unsigned long flags);
- 全局并发队列的优先级
DISPATCH_QUEUE_PRIORITY_HIGH 2 //高
DISPATCH_QUEUE_PRIORITY_DEFAULT 0 //默认(中)
DISPATCH_QUEUE_PRIORITY_LOW (-2) //底
DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN //后台
四个比较容易混淆的专业术语
- 同步、异步:是否具备开启多线程的能力,会不会阻塞当前线程的执行,直到Block执行完毕
- 并发、串行:决定任务的执行方式
同步,异步,并发,串行的组合
- 1、同步并发(了解)
//全局并发队列(失去并发功能,因为并发的前提是多线程)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//同步函数(不会开子线程)
dispatch_sync(queue, ^{
//code
});
- 2、同步串行(了解)
//手动创建串行队列
dispatch_queue_t queue = dispatch_queue_create("myThread", NULL);
//同步函数(不会开子线程)
dispatch_sync(queue, ^{
//code
});
- 3、异步并发(最常用)
//全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//异步函数(一般开多条子线程来同时执行任务)
dispatch_async(queue, ^{
//code
});
- 4、异步串行(偶尔使用)
//手动创建串行队列
dispatch_queue_t queue = dispatch_queue_create("myThread", NULL);
//异步函数(会开线程,但一般只开一条子线程)
dispatch_async(queue, ^{
//code
});
-
5、异步函数中的主队列(常用,一般用在线程间通信)
- 不会开线程
- 是GCD自带的一种特殊串行队列,放在住队列中的任务都会放在主线程中执行;使用dispatch_get_main_queue()获得主队列
- 只要是在主队列执行的任务都会自动放到主线程中去执行,即使是添加到异步函数中执行
6、同步函数与主队列(
不能这样使用
)
- (void)syncMainQueue
{
NSLog(@"syncMainQueue----begin--");
// 1.主队列(添加到主队列中的任务,都会自动放到主线程中去执行)
dispatch_queue_t mainQueue = dispatch_get_main_queue();
// 2.添加 任务 到主队列中 异步 执行
dispatch_sync(mainQueue, ^{
NSLog(@"-----下载图片1---%@", [NSThread currentThread]);
});
NSLog(@"syncMainQueue----end--");
}
- 卡死(死锁)的原因:因为
syncMainQueue
方法是在主线程执行的,但是执行到第5行的时候就卡死了,因为第5行把一个新的任务放到主线程的后面,而且线程的执行任务是遵循FIFO原则,所以只有等syncMainQueue
方法执行完毕后才执行NSLog(@"-----下载图片1---%@", [NSThread currentThread])
;但是syncMainQueue
想执行完,必须要执行NSLog(@"-----下载图片1---%@", [NSThread currentThread])
,因为NSLog
是在同步函数中,所以需要立刻执行,所以就会出现你等我执行完,我等你执行完的现象,从而导致卡死状态。
队列组
- 概念:把不同的任务放在队列里面,然后把该队列放进队列组里面
- 优点:队列组会等待队列里面的所有任务执行完毕后才调用另一个函数
- 使用场景举例:等下载图片1任务与现在图片2任务都完成后回到主线程进行合成照片
//创建队列组
dispatch_group_t group = dispatch_group_create();
//全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//任务1
dispatch_group_async(group, queue, ^{
//下载图片1
});
dispatch_group_async(group, queue, ^{
//下载图片2
});
//合并图片
dispatch_group_notify(group, queue, ^{
//合并图片
});
//回到主线程刷新界面
dispatch_async(dispatch_get_main_queue(), ^{
//刷新
});
GCD概览
同步/异步函数 | 全局并发队列 | 手动创建串行队列 | 主队列 |
---|---|---|---|
同步函数 | 没有开启新的线程;串行执行任务 | 没有开启新的线程;串行执行任务 | 没有开启新的线程;串行执行任务 |
异步函数 | 有开启新的线程;并发执行任务 | 有开启新的线程;串行执行任务 | 有开启新的线程;串行执行任务 |
NSOperation
- NSOperation是抽象类,只能使用它的子类(NSInvocationOperation,NSBlockOperation,自定义继承NSOperation的子类)
- NSInvocationOperation可以脱离队列来执行任务,NSInvocationOperation是用来封装任务的,如果调用start方法,则会把任务放到主线程调用;只有放到队列中才在子线程执行。
- NSBlockOperation也可以脱离队列来执行任务,一个操作可以有多个任务,
每个操作
的第一个任务是在主线程执行;而且还可以追加任务,追加的任务是在子线程执行
- (void)cancel;//取消单个操作
NSOperationQueue
只要是自己创建的队列,则任务就会在子线程中执行,而且是默认并发执行,除非最大并发数设置为1;
将操作放到队列中
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
-
最大并发数
- 表示
同一时间
最多能开多少条线程
- 表示
-
依赖
- 需求:影响操作的执行顺序
//operationB依赖操作operationA
[operationB addDependency:operationA];
//移除依赖
[operationB removeDependency:operationA];
- 注意点:不能相互依赖
- 相对GCD的障栏,它可以在不同的队列创建依赖
- 等操作A完成了再执行操作B,可以这样写
//@property (copy) void (^completionBlock)(void)
[opetationA setCompletionBlock:^{
//opetationB
}]
- 队列相关方法
- (void)cancelAllOperations;//取消所有操作,一般用在内存警告
方法里面
@property (getter=isSuspended) BOOL suspended;//暂停(或者恢复)操作,一般在开始(完成)刷新UI界面的时候调用
单例模式
ARC环境下得单例模式:
概念:可以保证在程序运行的过程中,一个类只有一个实例(与懒加载不一样)
单例模式的本质:为什么每次创建一个对象的内存地址不一样,本质上是因为alloc方法,此方法是为对象分配内存,所以我们应该重写alloc方法来控制一个类只有一个实例对象
alloc方法内部调用的其实是
+(instancetype)allocWithZone:(struct _NSZone *)zone
方法为了防止多线程的问题,所以应该为创建单例对象的方法添加互斥锁
//可以使用GCD实现,此处没采用
static id _instance;//全局变量
if (_instance == nil) { // 防止频繁加锁
@synchronized(self) {//互斥锁
if (_instance == nil) { // 防止创建多次
_instance = [super allocWithZone:zone];
}
}
}
- 防止实例对象调用copy产生新对象,我们应该重写
-(id)copyWithZone:(NSZone *)zone
和-(id)mutableCopyWithZone:(NSZone *)zone
来保证实例的唯一性,所以这两个方法直接返回以上的_instance
即可;
MRC环境下得单例模式:
- 目的:为了防止单例对象被release或者retain
- 跟在ARC环境下类似,只需要重写几个方法即可
- (oneway void)release{
}
- (id)autorelease{
return self;
}
- (id)retain{
return _instance;//return self;
}
- (NSUInteger)retainCount{
return 1;
}
简化单例模式代码
- 把单例代码抽成宏,宏最好写在一个单独的.h文件,分两部分写,一个是.h文件,一个是.m文件的
- 什么代码比较适合抽成宏?百年不变的代码,因为宏里面写错了,不会具体报哪行错了,所以不利于调试。(不一样的地方可以传参数解决)
- 该宏应该做好环境适配(ARC/MRC)
#if __has_feature(objc_arc)
//arc环境
#else
//mrc环境
#endif
互斥锁
- 使用场景:多线程抢夺同一资源的时候才用,并不是开了多线程就一定要加锁
- 只要加上互斥锁,就会消耗性能,所以苹果不推荐使用,一般在服务端使用,移动端很少使用
- 只要被@synchronized的{}包裹起来的代码,同一时刻只能被一个线程执行
- 加锁必须要传递一个锁对象(
任何对象都可以充当锁对象
),用来作为锁,但是多个线程必须使用同一把锁
才行,否则失效;开发过程中我们一般使用self来当做锁对象,因为很多时候self就是代表控制器,所以是唯一对象,如
@synchronized(self){
/*
code,需要被锁的代码
*/
}
- 加锁的时候尽量缩小范围(作用域),因为范围越大性能越底,尽量只在影响线程安全(多线程同时抢夺同一资源)的代码处加锁。
- 优点:能够有效防止因多线程抢夺资源造成的数据安全问题
- 缺点:需要消耗大量的CPU资源
- 相关专业术语:
线程同步
(多条线程在同一条线上执行,即按顺序执行任务,即只有一条线程执行完毕才到另外一条线程来执行)与线程并发
相反;加锁就可以实现线程同步
原子性和非原子性
- nonatomic: 非线性安全,不会为setter加锁,取而代之的可以在局部地方进行加锁,因为setter的调用频率比较高,所以会消耗大量的性能
- atomic:线程安全,会为setter加锁,系统默认会加锁;需要消耗大量的性能;是系统自动给我们添加的锁,但不是互斥锁,是自旋锁;
- 共同点:都能够保证多线程在同一时间内只能有一个线程操作锁定的代码;
- 不同点:如果互斥锁,假如现在被锁住了,那么后面来的线程就会进入“休眠”状态,直到解锁之后,就会唤醒线程继续执行;如果是自旋锁,假如现在被锁住了,那么后面来的线程不会会进入“休眠”状态,到解锁之后,就会直接执行;
- 自旋锁更加适合做一些时间较短的操作;
线程间通信
- 举例:在子线程加载图片,在主线程刷新UI界面
//下载完成后将image传到主线程,并在downloadFinished:方法内刷新UI界面
//waitUntilDone参数:若传YES则表明:等待主线程里面的图片设置完成后再执行子线程之后的代码;传NO就不会等待主线程的操作,便直接执行子线程的代码
[self performSelectorOnMainThread:@selector(downloadFinished:) withObject:image waitUntilDone:NO];
//也可以这样优化
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO];
//线程间通信的常用方法一(performSelector):
[self performSelector:@selector(downloadFinished:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
//线程间通信的常用方法二(NSOperationQueue):
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
// 1.异步下载图片(耗时操作)
NSURL *url = [NSURL URLWithString:@"图片URL"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 2.回到主线程,显示图片(刷新UI)
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
//线程间通信的常用方法三(GCD):
dispatch_sync(dispatch_get_main_queue(), ^{
//操作
});