下文中,前面介绍一些简单的概念。虽然简单,但还是需要了解。
后面介绍的pthread简单了解即可。GCD和NSOperation需要熟悉掌握如何运用。
不喜欢说很多引入的话,习惯用记笔记的方式,让文章满满干货不影响学习。首先是基本概念:
一.基本概念
1.基本概念
1.1进程
- 在系统中正在运行的一个应用程序
- 每个进程之间是独立的,均运行在其专用且受保护的内存空间内
1.2线程
在线程中真正执行任务的
1.3进程和线程的关系
包含关系:进程内有多条线程,至少有一条
1.4多线程
一个进程中可以开启多条线程,每条线程可以并行执行不同的任务。
多线程原理
- 同一时间,CPU只能处理1条线程,只有1条线程在工作;
- 多线程并发执行,其实是CPU快速的在多条线程之间调度(切换)
- 如果CPU调度线程足够快,就有多线程并发执行的假象
注意
多线程数量不少越多越好,一般控制在3-5条。太多反而会降低每条线程的调度次数,降低效率。
优点
适当的提高程序的执行效率,CPU、内存的利用率。
缺点
- 创建线程有开销:内存方面、时间方面都有
- 如果开启大量线程,CPU会频繁调度线程,降低程序的性能
1.5主线程/UI线程、子线程
一个程序运行后,会默认开启一条线程,称为主线程/UI线程。
-
主线程作用:显示/刷新UI、处理事件(点击、滚动)
-
主线程实用注意:
1.不要将耗时操作放到主线程,会导致界面卡顿;
2.所有UI操作必须在主线程中执行; -
子线程/后台线程/非主线程:只有一条主线程,主线程以外的就是子线程/后台线程
二.线程的状态
线程状态有:新建、就绪、运行、阻塞、死亡 5个状态。
-
如图【就绪状态、运行状态切换】当线程新建,开启线程后就进入“就绪状态”,CPU调度当前线程,就进入“运行状态”;
-
当CPU调度其他线程到时候,当前线程就恢复到就绪状态;
就绪状态、运行状态切换 -
如图【阻塞状态】,当调用了sleep方法、或者等待同步锁的时候,运行中的线程切换为阻塞状态;线程对象移出可调度线程池。
-
如图【所有状态】,阻塞状态的线程sleep到时、得到同步锁的时候,状态就由阻塞状态切换为就绪状态;线程状态回到可调度线程池,可让CPU调度当前线程;
注意:
阻塞状态需要切换到就绪状态后,才可以切换到运行状态。阻塞状态不能直接转换为运行状态; -
如图【所有状态】,当现场任务执行完毕,就会进入死亡状态
2.多线程
- 如下图,多线程有这几种实现方案:pthread、NSThread、GCD、NSOperation。
- GCD和NSOperation线程的生命周期是自动管理的,线程内部的任务执行完毕的时候,线程会释放,使用频率也高一些。
- pthread几乎不用,所以pthread只是简单的介绍。
三.pthread的使用
pthread使用较少,简单了解就行。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//phread创建线程
/*
01 包含头文件
02 创建线程对象
*/
pthread_t thread = nil;
//03 创建线程
/*
第一个参数:线程对象的地址
第二个参数:线程的属性(优先级)
第三个参数:指向函数的指针 void *(*)(void *)
第四个参数:传递给函数的参数
*/
pthread_create(&thread, NULL, run, @"str");
}
//技巧:(*)改写成函数的名称,再补全参数
void *run(void * str){
NSLog(@"%@,str:%@",[NSThread currentThread],str);
return NULL;
}
四.NSThread
NSThread创建
1.动态创建
//创建线程对象
/*
第一个参数:目标对象
第一个参数:方法选择器
第一个参数:传递给线程方法的参数
*/
NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"123"];
//启动线程
[thread start];
2.静态创建
//静态创建
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"123"];
3.直接分离出一条子线程
- 在
当前线程
执行操作
[self performSelector:@selector(threadRun)];
[self performSelector:@selector(threadRun) withObject:nil];
[self performSelector:@selector(threadRun) withObject:nil afterDelay:2.0];
- (在其他线程中)指定主线程执行操作
[self performSelectorOnMainThread:@selector(threadRun) withObject:nil waitUntilDone:YES];
- (在主线程中)指定其他线程执行操作
//这里指定为某个线程
[self performSelector:@selector(threadRun) onThread:newThread withObject:nil waitUntilDone:YES];
//这里指定为后台线程
[self performSelectorInBackground:@selector(threadRun) withObject:nil];
NSThread常用方法
- 获取当前线程
[NSThread currentThread];
- 获取主线程
[NSThread mainThread];
- 进入sleep状态
[NSThread sleepForTimeInterval:1.0];
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
- 取消线程
[newThread cancel];
- 所有线程都停止
[NSThread exit];
- 设置名称
thread1.name = @"thread1";
- 设置优先级
范围0.0-1.0,默认0.5
优先级更高的线程,被CPU调度到的概率会更高
thread1.threadPriority = 1.0;
五.线程安全
当多个售票员在售同一趟火车的票时,为了避免售出同一张票,我们需要使用互斥锁来避免这个问题。
@synchronized(self){
锁住的代码块
}
- 互斥锁使用的前提:多条线程抢夺同一块资源;
- 锁定1份代码只用1把锁,用多把锁是无效的;
- 互斥锁的优点:让多条线程按顺序执行任务。防止因多线程抢夺资源造成的数据安全问题;
- 互斥锁的缺点:需要消耗大量的CPU资源;
互斥锁使用实例
创建3个线程代表3个售票员,可以售出的总票数totalCount为100。
3个线程都调用-(void)saleTicket{}方法。
- (void)viewDidLoad {
[super viewDidLoad];
//设置总票数
self.totalCount = 100;
self.thread01 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread02 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread03 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread01.name = @"售票员1";
self.thread02.name = @"售票员2";
self.thread03.name = @"售票员3";
//线程启动
[self.thread01 start];
[self.thread02 start];
[self.thread03 start];
}
saleTicket方法里面,while循环减少toltalCount,直到totalCount = 1。
3个线程都在调用saleTicket方法,为了避免重复减少就给减少部分的代码添加互斥锁。
@synchronized(self){}加锁的位置需要注意,saleTicket方法里面,如果加锁在while之前,就是要一个线程把while循环跳出去,即totalCount数量都减为0,下一个线程才能调用。那就变成了一个售票员把所有的票卖完了。
如果把锁加在while之后,一个线程循环了一遍,另一个线程立马就可以来进行下一遍循环了。不会导致一个售票员把所有的票卖完了。
-
注意:加多把锁是无效的;加锁的位置;多个线程抢夺资源的时候才需要加锁;
加锁的代码块,就是我在执行这部分的时候,你就不要执行这里。等我执行完这里你再来
-(void)saleTicket{
//为代码添加互斥锁(互斥锁)
//token:锁对象(要使用全局的对象),建议直接使用self
//{}要加的代码块(就是我在执行这部分的时候,你就不要执行这里。等我执行完这里你再来)
//当锁打开之后,会主动唤醒排队的线程
while (1) {
@synchronized(self){
//售票
//检查余票,如果有票就卖出,否则提示用户
if (self.totalCount >0) {
self.totalCount --;
NSLog(@"%@售出一张,剩余:%ld",[NSThread currentThread].name,(long)self.totalCount);
}else{
NSLog(@"%@已卖完",[NSThread currentThread].name);
break;
}
}
}
}
六.GCD
GCD中增加了任务
、队列
两个概念。
- 任务:即要做的事情;
- 队列:用于存放任务;
封装任务的函数:同步、异步
封装任务的函数有同步函数、异步函数。
-
同步函数:dispatch_sync
1)不具备开线程的能力,不能开线程
2)执行任务的方式:同步
3)会阻塞当前线程并等待block中的任务执行完毕,然后当前线程才会继续往下运行。
-
异步函数:dispatch_async
1)具备开线程的能力,可以开启线程
2)执行任务方式:异步
3)使用异步操作,当前线程会直接往下执行,不会阻塞当前线程。
GCD中的队列:并发、串行
- 并发队列:任务可以同时执行
GCD会FIFO先进先出的取出任务,只要第一个任务取出来之后,不用等待执行完,就可以接着取出第二个任务执行。CPU在多个线程之间切换调度,如果速度够快,看起来所有任务都是一起执行的。
- 有两种并发队列:
1)自己创建: dispatch_queue_create方法、DISPATCH_QUEUE_CONCURRENT类型
2)全局并发队列:dispatch_get_global_queue
- 串行队列:任务必须一个接着一个的执行
GCD会FIFO先进先出的方式取出任务,第一个任务取出来之后,必须等待该任务执行完,才可以接着执行第二个任务
- 串行队列有以下两种类型:
1)自己创建:dispatch_queue_create方法、DISPATCH_QUEUE_SERIAL类型
2)主队列:特殊的串行队列(和主线程相关联的)
放在主队列中的任务,都会放在主线程中执行
不同任务、不同队列组合
不同的任务和不同的队列方式组合效果如下,原因后续分析:
异步 | 同步 | |
---|---|---|
串行 | 开启一条子线程,在子线程按顺序一个个执行 | 在当前线程 ,按顺序一个个执行 |
并发 | 开启多条子线程,多个子线程同时执行(开启线程的个数不等于子线程的个数,由GCD自己决定的) | 在当前线程 ,按顺序一个个执行 |
主队列 | 在主线程 按顺序一个个串行执行 |
死锁 |
总结:
可以开线程的情况:异步函数,且是非主队列;
开线程条数:看队列,并行队列开N条,串行开1条;
1.异步+串行
GCD开启线程都是先创建队列,然后把任务添加到队列里面。
-
自己创建队列:
dispatch_queue_create第一个参数是:队列名称;
第二个参数是队列类型:
DISPATCH_QUEUE_CONCURRENT 并行
DISPATCH_QUEUE_SERIAL 串行 -
异步任务:dispatch_async
-(void)asyncSerial{
//01-创建队列
dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_SERIAL);
//02-封装任务,把任务添加到队列
dispatch_async(que, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"3----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"4----%@",[NSThread currentThread]);
});
}
串行+异步
当number = 1,name = main的时候是主线程。
上面异步+串行的方式,从打印可以看出:开启了一个线程,在这个线程中按顺序串行执行任务;
2.异步+并发
-(void)asyncConcurent{
//01-创建队列
NSLog(@"--start---");
dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_CONCURRENT);
//02-封装任务,把任务添加到队列
dispatch_async(que, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"3----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"4----%@",[NSThread currentThread]);
});
NSLog(@"--end---");
}
异步+并发
如上图:异步+并发 开启多条子线程,所有任务是并发执行的
3.异步+主队列
-(void)asyncMain{
dispatch_queue_t que = dispatch_get_main_queue();
//02-封装任务,把任务添加到队列
dispatch_async(que, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"3----%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"4----%@",[NSThread currentThread]);
});
}
异步+主队列
主队列是串行的,添加在主队列中的任务都是在主线程中执行的。从上图也可以看出,异步+主队列 是在主线程按顺序一个个执行的。
4. 同步+ 串行
-(void)syncSerial{
NSLog(@"--start---");
dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_SERIAL);
//02-封装任务,把任务添加到队列
dispatch_sync(que, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"3----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"4----%@",[NSThread currentThread]);
});
NSLog(@"--end---");
}
同步+串行
同步不具备开启线程的能力,所以不开启线程;
同步会阻塞任务,需要一个任务执行完才能执行下一个;
从上图也可以看出,同步+串行 是在当前线程按顺序一个个执行任务。
5. 同步+ 并发
-(void)syncCurrent{
//01-创建队列
NSLog(@"--start---");
dispatch_queue_t que = dispatch_queue_create("que1", DISPATCH_QUEUE_CONCURRENT);
//02-封装任务,把任务添加到队列
dispatch_sync(que, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"3----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"4----%@",[NSThread currentThread]);
});
NSLog(@"--end---");
}
同步+并发
从上图也可以看出,同步+ 并发和 同步串行一样, 是在当前线程按顺序一个个执行任务。
6. 同步+ 主队列
-(void)syncMain{
//01-创建队列
dispatch_queue_t que = dispatch_get_main_queue();
NSLog(@"---------");
//02-封装任务,把任务添加到队列
dispatch_sync(que, ^{
NSLog(@"1----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"2----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"3----%@",[NSThread currentThread]);
});
dispatch_sync(que, ^{
NSLog(@"4----%@",[NSThread currentThread]);
});
}
同步+主队列-->死锁
如上图,同步+主队列造成了死锁。
前提:
首先,同步会造成阻塞,要当前任务执行完之后才能执行下一个任务。
再次,主队列是串行的,一个任务执行完才能执行下一个。
主队列上面添加的任务,都是在主线程执行。
-
代码执行到257行的时候,因为是串行的,会把257行的任务添加到主队列中。等待主队列中当前的任务执行完后执行257行的任务。即到270行syncMain方法执行完后,执行257行添加的任务。
-
257行的任务是同步的,会阻塞当前线程。syncMain要执行完的前提是257行的任务执行完才可以。
所以257行的任务和syncMain都在等待对方执行完之后再执行。造成了死锁。
6.1 假设把上面同步+主队列的方法syncMain放到后台线程去调用,会怎么样?
- (void)viewDidLoad {
[super viewDidLoad];
//直接调用同步+主队列的方法:死锁
//[self syncMain];
//把方法放到后台线程执行:不会死锁
[self performSelectorInBackground:@selector(syncMain) withObject:nil];
}
同步+主队列 放到后台线程调用
执行结果如上图,串行执行任务。
我们是在后台线程,对主线程添加同步任务。
- 当我们添加257行的同步任务到主线程时,syncMain方法不在主线程,不需要等待syncMain方法执行完后再执行257行的任务。
主线程此时没有在执行的任务,就执行了257行这个任务。所以syncMain方法就可以执行完成。就不会造成死锁。
6.2 为什么“3.异步+主队列”没有造成死锁呢?
3中是异步方式,不能对线程造成阻塞,所以不会造成死锁。
上面就介绍完同步、异步和串行、并发各种组合的结果。下面开始查看GCD的其他用法。
GCD-栅栏函数
- 栅栏函数如下:
dispatch_async(que, ^{
});
-
栅栏函数作用:
【并发队列+异步】的情况下,开启多条线程并发执行任务的时候,栅栏函数可以设定某几个任务并发执行后,再并发执行某几个任务。 -
举例
例如,有4个任务是并发+异步的,那么会开启多条线程来并发执行这4个任务。要求,1、2并发执行完成,再并发执行任务3、4.
//开启并发队列
dispatch_queue_t que = dispatch_queue_create("que", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(que, ^{
NSLog(@"1--------%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"2--------%@",[NSThread currentThread]);
});
//添加栅栏函数
dispatch_barrier_sync(que, ^{
NSLog(@"++++++++");
});
dispatch_async(que, ^{
NSLog(@"3--------%@",[NSThread currentThread]);
});
dispatch_async(que, ^{
NSLog(@"4--------%@",[NSThread currentThread]);
});
首先,我们看未添加栅栏函数的打印结果:
任务执行顺序是3、1、2、4。是开启多条线程并发执行的。
未添加栅栏函数的打印结果
再看添加栅栏函数后的打印结果:
任务执行顺序是1、2、栅栏函数的任务、4、3。
达到目的任务1、2并发执行后再让3、4并发执行。
添加栅栏函数的打印结果
从上面可以看出,使用栅栏函数:
栅栏函数前面的任务执行完之后,执行栅栏任务,再执行栅栏函数后面的任务;
注意
栅栏函数使用的时候,不能使用全局并发队列dispatch_get_global_queue(0, 0);
;否则拦截不到任务
思考:栅栏函数要求是【异步+并发】,那为什么不能【同步+并发】呢,因为【同步+并发】组合就是在当前线程顺序执行了,没有必要使用栅栏函数来限制任务的执行顺序。同理,【异步+串行】、【同步+串行】添加栅栏函数没有意义。
GCD-快速迭代
平时我们会使用for循环来进行遍历,这相当于在当前线程串行执行。
GCD提供了一个快速迭代函数,让多个线程并发遍历。提高效率。
- 快速迭代函数:
dispatch_apply(10, que, ^(size_t i) {
});
第一个参数:上面的10代表遍历10次
第二个参数:在哪个队列;我们使用并发队列,达到开启多个线程的效果
第三个参数:上面的i相当于for循环里面的i,遍历到第几个
- 举例
dispatch_queue_t que = dispatch_get_global_queue(0, 0);//全局并发
// dispatch_queue_t que = dispatch_queue_create("queName", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(10, que, ^(size_t i) {//i相当于执行到第几次
NSLog(@"%zu----%@",i,[NSThread currentThread]);
});
并发队列+快速遍历函数
上面例子中【并发+快速迭代】是开启多个子线程,和主线程一起并发执行任务。
注意
如果使用【主队列+快速迭代】会造成死锁
注意
如果使用【串行队列+快速迭代】:在主线程串行执行,和for循环一样的效果
GCD-队列组
上面讲述了GCD中的两大概念,任务、队列。现在介绍的队列组,就是来管理队列中的任务。
-
使用步骤
1.创建队列组
2.创建队列
3.创建队列组异步函数,关联对应的队列、队列组 -
作用:监听任务的完成
当一个组中的所有任务执行完成之后,可以通过监听函数得到。 dispatch_group_notify(group, que2, ^{ });
//01 创建队列组
dispatch_group_t group = dispatch_group_create();
//02 创建队列
dispatch_queue_t que2 = dispatch_queue_create("que2", DISPATCH_QUEUE_CONCURRENT);
//03 封装任务、并监听任务的队列情况
dispatch_group_async(group, que2, ^{
NSLog(@"1--------%@",[NSThread currentThread]);
});
dispatch_group_async(group, que2, ^{
NSLog(@"2--------%@",[NSThread currentThread]);
});
dispatch_group_async(group, que2, ^{
NSLog(@"3--------%@",[NSThread currentThread]);
});
//拦截通知:
//这个函数里面的队列决定这里的block块在哪个线程执行(主线程、非主线程)
//dispatch_group_notify:异步执行的(如下,先打印的end,再执行dispatch_group_notify内容的)
dispatch_group_notify(group, que2, ^{
NSLog(@"队列组中的任务都执行完毕-%@",[NSThread currentThread]);
});
NSLog(@"--end--");
- 了解--队列组其他可以达到监听效果的写法
也可以使用:dispatch_group_enter、dispatch_group_leave组合起来,达到监听的作用。
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t que2 = dispatch_queue_create("que2", DISPATCH_QUEUE_CONCURRENT);
//在该方法后面的任务会被队列组监听
//dispatch_group_enter、dispatch_group_leave要成对出现
dispatch_group_enter(group);
dispatch_group_async(group, que2, ^{
NSLog(@"2--------%@",[NSThread currentThread]);
//监听到该任务已经执行完毕
dispatch_group_leave(group);
});
dispatch_group_notify(group, que2, ^{
NSLog(@"队列组中的任务都执行完毕-%@",[NSThread currentThread]);
});
- 了解--队列组调用函数方法
队列组里面的任务可以是block的,也可以是调用函数的,如下:
-(void)other{
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t que2 = dispatch_queue_create("que2", DISPATCH_QUEUE_CONCURRENT);
//封装任务 (block方法)
dispatch_group_async(group, que2, ^{
NSLog(@"1--------%@",[NSThread currentThread]);
});
//封装任务(函数方法)
dispatch_group_async_f(group, que2, NULL, run);
}
void run(void * name){
NSLog(@"2--------%@----%@",[NSThread currentThread],name);
}
非队列组的任务封装,也可以是函数的方式
//封装任务(函数的方式)
dispatch_async_f(que2, NULL, run);
七. NSOperation和NSOperationQueue
NSOperation和NSOperationQueue配合使用,也可以实现多线程编程。
1.NSOperation
NSOperation是一个抽象类,不具备封装操作的能力,需要使用其子类NSInvocationOperation、NSBlockOperation、自定义NSOperation实现内部响应的方法。
使用步骤
1.封装操作
2.执行操作(start方法)
1.1 NSInvocationOperation
//01 封装操作对象
NSInvocationOperation *op1 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download1) object:nil];
//02 执行操作
[op1 start];
1.2 NSBlockOperation
//01 封装操作对象
NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"block2----%@",[NSThread currentThread]);
}];
//添加任务
[op2 addExecutionBlock:^{
NSLog(@"block3----%@",[NSThread currentThread]);
}];
//02 执行操作
[op2 start];
操作、任务
GCD中有任务、队列。对于NSOpration来说,有操作、队列。其中:
- 操作:一个NSOperation对象就是一个操作
- 任务:一个block就是一个任务。
上面的NSBlockOperation例子中就有1个操作,2个任务。 -
当一个操作中的任务数量大于1的时候,就会开启子线程来和当前的线程一起完成任务。
上面的例子打印结果如下:
1个操作中有两个任务,开启了子线程和主线程一起执行任务。
1个操作、2个任务打印结果
1.3 自定义操作
参考下文的【4.4自定义操作、以及注意事项】
2.NSOperationQueue
上面讲到NSOperation和NSOperationQueue实现多线程。
使用步骤如下:
1.创建队列NSOperationQueue
2.封装操作对象NSOperation
3.把操作添加到NSOperationQueue中
(系统会自动将NSOperationQueue中的操作NSOperation取出来,放到线程中执行)
2.1 NSOperationQueue和NSInvocationOperation的使用
//01 创建队列
NSOperationQueue * que = [[NSOperationQueue alloc] init];
//02 封装操作
NSInvocationOperation * op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download1) object:nil];
NSInvocationOperation * op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download2) object:nil];
NSInvocationOperation * op3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download3) object:nil];
//03 把操作放到队列
[que addOperation:op1];
[que addOperation:op2];
[que addOperation:op3];
-(void)download1{
NSLog(@"download1:%@",[NSThread currentThread]);
}
-(void)download2{
NSLog(@"download2:%@",[NSThread currentThread]);
}
-(void)download3{
NSLog(@"download3:%@",[NSThread currentThread]);
}
NSOperationQueue和NSInvocationOperation使用打印
上面打印结果可看出,开启了多条线程同时执行。为并发。
2.2 NSOperationQueue和NSBlockOperation的使用
//01 创建队列
NSOperationQueue * que = [[NSOperationQueue alloc] init];
//02 封装操作
NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op1:%@",[NSThread currentThread]);
}];
//03 把操作添加到队列
[que addOperation:op1];//该方法内部会自动执行start方法
2.3 addOperationWithBlock
上面封装操作、把操作添加到队列这两部,可以简化为一步,用addOperationWithBlock代替,如下:
//01 创建队列
NSOperationQueue * que = [[NSOperationQueue alloc] init];
//blockque的简便方法
[que addOperationWithBlock:^{
//该方法内部,会把block内部的任务封装成一个操作(operation),然后把操作添加到队列
NSLog(@"add in que:%@",[NSThread currentThread]);
}];
3. maxConcurrentOperationCount
3.1并发、串行
GCD的队列:
回顾GCD的队列,有以下两种:
1).并发:自己创建、全局并发
2).串行:自己创建、主队列
NSOperationQueue队列
1).并发:NSOperationQueue中的队列有两种,上面我们一直使用的都是自定义队列。默认为并行。即开启多条子线程同时执行。
2)串行:当我们需要串行队列的时候,maxConcurrentOperationCount = 1即可。
队列类型 | 创建方法 | 特点 |
---|---|---|
自定义队列 | [[NSOperationQueue alloc] init] | 默认为并发队列,可以设置通过maxConcurrentOperationCount为串行队列 |
主队列 | [NSOperationQueue mainQueue] | 串行队列,和主线程相关 |
注意:开启几条子线程并不是由操作数量决定,由系统自己决定
3.2maxConcurrentOperationCount举例
- 先封装几个操作添加到队列,查看到打印结果为:开启多条线程,并发执行。
//01 创建队列
NSOperationQueue * que = [[NSOperationQueue alloc] init];
//02 封装操作
NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op1:%@",[NSThread currentThread]);
}];
NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op2:%@",[NSThread currentThread]);
}];
NSBlockOperation * op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op3:%@",[NSThread currentThread]);
}];
//03 把操作添加到队列
[que addOperation:op1];//该方法内部会自动执行start方法
[que addOperation:op2];
[que addOperation:op3];
封装多个操作的打印结果
- 我们再上面添加maxConcurrentOperationCount = 1,设置为串行。看打印结果:开启多个线程,但是串行执行。
que.maxConcurrentOperationCount = 1;
串行打印结果
- 在上面的基础上,给op2添加两个任务。查看maxConcurrentOperationCount = 1是否还能串行执行任务。
查看下面的打印界面,即时maxConcurrentOperationCount = 1了,但是对于任务数量大于1的操作是无效的。
[op2 addExecutionBlock:^{
NSLog(@"op2 add1111111111111:%@",[NSThread currentThread]);
}];
[op2 addExecutionBlock:^{
NSLog(@"op2 add22222222:%@",[NSThread currentThread]);
}];
给op2添加两个任务
总结:
1.设置最大并发数,对于任务数量大于1的操作是无效的
即时maxConcurrentOperationCount = 1,任务数量大于1的操作也不会串行执行。
2.que.maxConcurrentOperationCount = 1;只能决定是串行还是并发,不能决定开几条子线程。开几条线程,还是由系统决定。
3.que.maxConcurrentOperationCount = 0; 设置为0,就是不执行
4.que.maxConcurrentOperationCount = -1;默认为-1,就是最大值,系统根据情况自己开线程。
4. NSOperationQueue的其他操作
NSOperationQueue还要GCD没有的功能,例如:暂停、取消。
4.1暂停
[self.que setSuspended:YES];
4.2取消暂停
[self.que setSuspended:NO];
4.3取消操作
这是取消全部操作。
[self.que cancelAllOperations];
暂停:只能暂停当前操作后面的操作;当前操作不可分割,必须执行完毕
操作是有状态的,有正在执行的、在等待的。
取消:只能取消正在等待的操作。已经执行的操作是不能取消的
4.4自定义操作、以及注意事项
自定义步骤:
1.新建操作类继承NSOperationQueue
2.子类里面重写-(void)main方法。把操作的任务写在这个方法里面。(把操作通过 [que addOperation:op]添加到队列,会自动调用 [op start];方法,start方法会自动调用这个main方法)
- 自定义操作好处:代码复用
注意事项:
在自定义操作的时候,每执行完一个耗时操作就判断一下当前操作是否被取消,如果被取消就直接返回。
去判断是否被取消的原因是,队列使用了自定义的操作。即时调用取消方法,也不会被取消。所以需要判断状态,被取消之后强制退出任务。
如下: LYTOperation : NSOperation,main方法里面进行状态判断。
.h:
@interface LYTOperation : NSOperation
@end
.m:
-(void)main{
for (int i = 0 ; i< 10000; ++i) {
NSLog(@"1:----%d:%@",i,[NSThread currentThread]);
}
//官方建议:在自定义操作的时候,每执行完一个耗时操作就判断一下当前操作是否被取消,如果被取消就直接返回
if (self.cancelled) {
return;
}
for (int i = 0 ; i< 10000; ++i) {
NSLog(@"2:----%d:%@",i,[NSThread currentThread]);
}
}
viewcontroller里面使用这个自定义操作,按钮点击可以取消操作:
#import "LYTOperation.h"
@interface ViewController ()
@property(nonatomic,strong)NSOperationQueue * que;
@end
- (void)viewDidLoad {
[super viewDidLoad];
//使用自定义队列
NSOperationQueue * que = [[NSOperationQueue alloc] init];
//封装操作
LYTOperation * op = [[LYTOperation alloc] init];
//把操作添加到队列
[que addOperation:op];//内部会调用start方法,start方法会调用main方法
self.que = que;
}
//取消
- (IBAction)cancel:(id)sender {
[self.que cancelAllOperations];
}
4. NSOperation添加依赖、监听
添加依赖
- 依赖:
如有多个任务,需要设置B执行完之后再执行A。我们就需要使用依赖。
如下:依赖B,B执行之后执行A
[A addDependency:B];
- 注意:
1)设置依赖要写在添加到队列之前
2)不能设置循环依赖。例如,操作A依赖操作B,不能再设置操作B依赖操作A。否则不会执行操作A和B。
3)可以跨队列添加依赖
- 举例:
队列1有操作:op1、op2、op3
队列2有操作:op4
要求执行顺序如下:op3、op2、op1、op4。
//01 创建队列
NSOperationQueue * que = [[NSOperationQueue alloc] init];
NSOperationQueue * que2 = [[NSOperationQueue alloc] init];
//封装对象
NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1:%@",[NSThread currentThread]);
}];
NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2:%@",[NSThread currentThread]);
}];
NSBlockOperation * op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3:%@",[NSThread currentThread]);
}];
NSBlockOperation * op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"4:%@",[NSThread currentThread]);
}];
//设置操作依赖
//如下,要求执行顺序为op3、op2、op1、op4
//[A addDependency:B]; 即A依赖B,B执行之后执行A
[op2 addDependency:op3];
[op1 addDependency:op2];
[op4 addDependency:op1];//跨队列依赖
//把操作添加到队列
[que addOperation:op1];
[que addOperation:op2];
[que addOperation:op3];
[que2 addOperation:op4];//放到了队列2
添加依赖执行效果
监听
监听某一个操作执行完,可以使用如下方法:
op3.completionBlock = ^{
};
- 举例:给上面例子中的op3添加监听:
//监听任务执行完毕
op3.completionBlock = ^{//op3执行完毕就来执行block这里
NSLog(@"执行完毕");
};
监听打印结果
GCD和NSOperation对比
GCD:
是C语言的API,block任务块表示。是轻量级的数据结构
1.监听队列中所有任务的完成
NSOperation:
NSOperation对于GCD是更加重量级的OC对象
1.可以为操作添加依赖
2.可以为操作添加监听是否完成
3.可以对操作取消、暂停
4.可以通过KVO对NSOperation对象精细控制。如监听当前操作是否已经被取消。
5.可以指定操作的优先级
6.可以自定义NSOperation的子类实现操作重用。
网友评论