题记:
“大笑的人可能被当做傻瓜;流泪可能被视为脆弱;主动认识他人,可能会让自己尴尬;去爱一个人,要冒被伤害的风险;想进步,就要冒险,人生最大的风险,就是拒绝一切冒险。”
来看下并发编程中异步线程的一种方式,还是理论知识,后续的内容完善之后,会着手去做一个下载器,并结合实际使用场景进行重构和优化来举例;
Operation Queues:
由于基于OC,因此基于Cocoa的应用通常会使用Operation Queues;
1.Operation Objects:
Operation object是NSOperation类的实例,封装了需要执行的任务及所需数据;
NSOperation是抽象基类,我们必须实现其子类;
1)NSOperation:
基类,用来自定义子类Operation object;继承NSOperation可以完全控制operation object的实现,包括修改操作执行和状态报告的方式;
Foundation framework 提供了两个具体的子类:
1)NSInvocationOperation:
可以直接使用的类,基于应用的一个对象和selector来创建operation object;如果你已经有现有的方法来执行需要的任务,就可以使用这个类;
NSInvocationOperation * theOp = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(myTaskMethod:) object:data];
2)NSBlockOperation:
可以直接使用的类,用来并发地执行一个或多个block对象;operation object使用“组”的语义来执行多个block对象,所有相关的block都执行完成之后,operation object才算完成;
NSBlockOperation * theOp = [NSBlockOperation blockOperationWithBlock:^{
//Do some work
NSLog(@"二:4-block1");
}];
[theOp addExecutionBlock:^{
//Do some other work
NSLog(@"二:4-block2");
}];
[theOp addExecutionBlock:^{
//Do some other work
NSLog(@"二:4-block3");
}];
[theOp addExecutionBlock:^{
//Do some other work
NSLog(@"二:4-block4");
}];
所有operation objects都支持以下关键特性:
1)支持建立基于图的operation objects依赖;可以阻止某个operation运行,直到它依赖的所有operation都已经完成;
2)支持可选的completion block,在operation的主任务完成后调用;
3)支持应用使用KVO通知来监听operation的执行状态;
4)支持operation优先级,从而影响相对的执行顺序;
5)支持取消,允许你终止正在执行的任务;
//二:
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
[queue setMaxConcurrentOperationCount:4];
//NSInvocationOperation
[queue addOperation:[[[MyCustomClass alloc]init] taskWithData:nil]];
//NSBlockOperation
//顺序执行: 如果需要顺序执行的话 可以添加多个NSBlockOperation,并添加彼此之间的依赖关系(注意不要有循环依赖)
NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"1");
}];
NSLog(@"%d",op1.isConcurrent);//0-NO
NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"2");
}];
[op2 setCompletionBlock:^{
NSLog(@"2:haha I am ok!");
}];
NSBlockOperation * op3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"3");
}];
NSBlockOperation * op4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"4");
}];
[op1 setQueuePriority:NSOperationQueuePriorityHigh];
[op4 addDependency:op3];
[op2 addDependency:op1];
[op3 addDependency:op2];
[queue addOperations:@[op1, op2 ,op3 ,op4] waitUntilFinished:YES];
2.并发 VS 非并发Operations:
通常将operation添加到operation queue中执行并发操作;
但也可以手动调用start方法来执行一个operation对象,这样做不保证operation会并发执行;
NSOperation类对象的isConcurrent方法告诉你这个operation相对于调用的start方法的线程,是同步还是异步执行的;isConcurrent方法默认返回NO,表示operation与调用线程同步执行(非并发);
如果需要实现并发operation,也就是相对调用线程异步执行的操作;需要添加额外代码,来异步的启动操作;
场景:
多数开发者从来都不需要实现并发operation对象,只需将operations添加到operation queue;
若提交非并发operation到,operation queue,queue也会创建线程来运行你的操作,以达到异步执行的目的;
只有当不希望使用operation queue来执行operation时,才需要定义并发operations;
解释:
并发(concurrency)和并行(parallellism)是:
解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
3.创建一个NSInvocationOperation对象:
场景:
有一个方法,需要并发地执行;
我们可以直接创建一个NSInvocationOperation对象,而不需要自己继承NSOperation;(如前所举示例)
4.创建一个NSBlockOperation对象:
NSBlockOperation对象用于封装一个或多个block对象;
一般创建时会添加至少一个block,然后根据需要添加多个block;(如前所举示例)
过程:
当NSBlockOperation对象执行时,会把所有的block提交到默认优先级的并发dispatch queue;然后NSBlockOperation对象等待所有block完成执行,最后标记自己已完成;因此使用block operation来跟踪一组执行中的block,类似thread join等待多个线程的结果;区别在于block operation本身也运行在一个单独的线程,应用其他线程在等待block operation完成时可以继续工作;
使用addExecutionBlock:可添加block;(如前所举示例)
如果需要顺序执行block,必须直接提交到所需的dispatch queue(这个貌似不好使,可以使用operation之间的依赖关系来保证执行顺序);(如前所举示例)
小结:
基于对象的任务,可以使用NSInvocationOperation;
纯执行任务,可以使用NSBlockOperation;
5.自定义Operation对象:
block operation 和 invocation operation不符合需求时,你可以直接继承NSOperation,添加任何想要的行为;
NSOperation提供了通用的子类继承点。而且实现了许多重要的基础设施来处理依赖和KVO通知;
继承所需的工作量主要取决于你要实现的是 非并发 还是 并发的 operation;
非并发:定义非并发operation简单些,只需执行主任务,并正确的响应取消事件;
并发:需要替换某些现有的基础设施代码;
6.执行主任务:
每个operation对象至少需要实现以下方法:
1)自定义initialization方法:初始化,将operation对象设置为已知状态;
2)自定义main方法:执行你的任务;
还可以选择性的实现以下方法:
1)main方法中需要调用的其他自定义方法;
2)Accessor方法:设置和访问operation对象的数据;
3)dealloc方法:清理operation对象分配的所有内存;
4)NSCoding协议的方法:允许operation对象archive和unarchive;
7.响应取消事件:
operation开始执行之后,会一直执行到任务完成,或者显示地取消操作;
尽管NSOperation提供了一个方法,让应用取消一个操作,但是识别出取消的事件这是我们的事;
如果operation直接终止,可能无法回收所有分配的内存或资源,因此operation对象需要检测取消事件,并优雅地退出执行;
operation对象定期地调用isCancelled方法,如果返回YES(表示已取消),则立即退出执行;
不管是自定义还是使用系统提供的两个具体类,都需要支持取消;
isCancelled方法,可频繁调用而不产生大的性能损失,以下地方可能需要调用:
1)在执行任何实际的工作之前;
2)在循环的每次迭代过程中,如果每次迭代相对较长可能需要调用多次;
3)代码中相对比较容易终止操作的任何地方;
8.为并发执行配置operations:
Operation对象默认按照同步方式执行,也就是在调用start方法的那个线程中直接执行;
由于operation queue为非并发operation提供了线程支持,对应用来说,多数operation仍然是异步执行的;
但是如果你希望手工执行operations,而且仍然希望能够异步执行操作,你就必须采取适当的措施,通过定义operation对象为并发操作来实现;
需要实现的方法(及描述)如下:
1)start:(必须)
所有并发操作都必须覆盖这个方法,以自定义的实现替换默认行为;手动执行一个操作时,你会调用start方法;因此对这个方法的实现是操作的起点,设置一个线程或其他执行环境来执行你的任务;你的实现在任何时候都绝对不能调用super;
2)main:(可选)
这个方法通常用来实现operation对象相关联的任务;尽管你可以在start方法中执行任务,使用main来实现任务可以让你的代码更加清晰地分离设置和任务代码;
3)isExecuting、isFinished:(必须)
并发操作负责设置自己的执行环境,并向外部client报告执行环境的状态。因此并发操作必须维护某些状态信息,以知道是否正在执行任务,是否已经完成任务。使用这两个方法报告自己的状态。这两个方法的实现必须能够在其他多个线程中同时调用。另外这些方法的状态变化时,还需要为相应的key path产生适当的KVO通知。
4)isConcurrent:(必须)
标识一个操作是否并发operation,覆盖这个方法并返回YES。
即使操作被取消,你也应该通知KVO observers,你的操作已经完成。当某个operation对象依赖另一个operation对象的完成时,他会监测后者的isFinished key path。如果你的operation对象没有产生完成通知,就会阻碍其他依赖你的operation对象运行。
注:(这里的写法运行起来,main方法并没有运行,感觉写的不太对,自定义并发Operation要处理的部分应该还有些)
9.维护KVO依从:
NSOperation类的key-value observing(KVO)依从于一下key paths:
1)isCancelled;
2)isConcurrent;
3)isExecuting;
4)isFinished;
5)isReady;
6)dependencies;
7)queuePriority;
8)completionBlock;
如果你覆盖start方法,或者对NSOperation对象的其他自定义运行(覆盖main除外),你必须确保自定义对象对这些key paths保留KVO依从。
覆盖start方法时,需要关注isExecuting和isFinished两个key paths;
如果你希望实现依赖于其他东西(非operation对象),你可以覆盖isReady方法,并强制返回NO,直到你等待的依赖得到满足。如果你需要保留默认的依赖管理系统,确保你调用了[super isReady]。当你的operation对象的准备就绪状态发生改变时,生成一个isReady的key paths的KVO通知。
除非你覆盖了addDependency:或removeDependency:方法,否则你不需要关注dependencies key path;
虽然可以生成NSOperation其他的KVO通知,但通常你不需要这样做。如果需要取消一个操作,你可以直接调用现有的cancel方法。类似的,你也很少需要修改queue优先级信息。最后,除非你的operation对象可以动态改变并发状态,你也并不需要提供isConcurrent key path的KVO通知。
第9条联系第8条,应该用到的地方比较少;
10.自定义一个Operation对象的执行行为:
对Operation对象的配置发生在创建对象之后,将其添加到queue之前;
1)配置Operation之间的依赖关系:
依赖关系可以顺序的执行相关的operation对象,依赖其他操作,则必须等到该操作完成之后自己才能开始;
可以创建一对一的依赖关系,也可以创建多个对象之间的依赖图。
使用NSOperation的addDependency:方法在两个operation对象之间建立依赖关系;
依赖关系不仅局限于相同queue中的operations对象,operation对象会管理自己的依赖,因此可以在不同的queue之间的operation对象创建依赖关系;
唯一的限制是不能创建循环引用;
一个operation对象的所有依赖都已经执行完,operation变成准备执行状态;如果operation已经在一个queue中,queue就可以在任何时候执行这个operation;如果你需要手动执行该operation,就自己调用operation的start方法;
配置依赖必须在运行operation和添加operation到queue之前进行,之后添加的依赖可能不起作用;
依赖要求每个operation对象在状态改变时必须发出适当的KVO通知,如果你自定义了operation对象的行为,就必须在自定义代码中生成适当的KVO通知,以确保依赖能够正确地执行;
2)修改operation的执行优先级:
对于添加到queue的Operations,执行顺序首先由已经入队的Operations是否准备好,然后再根据所有Operations的相对优先级确定。
是否准备好由对象的依赖关系确定,优先级等级则是operation对本身的一个属性;
默认所有的operation都拥有“普通”优先级,不过你可以通过setQueuePriority:方法来提升或降低operation对象的优先级
优先级只能应用于相同queue中的operations;
先满足依赖关系,然后再根据优先级从所有准备好的操作中选择优先级最高的那个执行;
3)修改底层线程优先级:
我们现在可以对operation底层线程的执行优先级进行配置,线程直接由内核管理,通常优先级高的线程会给予更对的机会;
对于operation对象,你指定线程优先级为0.0到1.0之间的某个数值,默认线程优先级为0.5;
要设置operation的线程优先级,你必须在将operation添加到queue之前,调用setThreadPriority:方法进行设置;
当queue执行该operation时,默认的start方法会使用你指定的值来修改当前线程的优先级;
不过新的线程优先级只在operation的main方法范围内有效;其他所有代码仍然(包括completion block)运行在默认优先级;
如果你创建了并发operation,并覆盖了start方法,你必须自己配置线程优先级(自定义并发operation,确实比较麻烦!);
4)设置一个completion block:
我们现在可以让operation在主任务完成之后执行一个completion block;可以使用这个block来执行任何不属于主任务的工作;
并发operation对象则可以使用这个block来产生最终的KVO通知;
调用NSOperation的setCompletionBlock:方法来设置一个completion block,你传递的block应该没有参数和返回值;
11.实现Operation对象的技巧:
1)Operation对象的内存管理:
Operation对象需要良好的内存管理策略。
2)创建自己的AutoRelease Pool:
Operation是OC对象,你需要在任务的代码中创建一个autorelease pool,这样可以保护那些autorelease对象得到尽快地释放。
@try {
@autoreleasepool {//do something
3)避免Per-Thread存储:
虽然多数operation都在线程中执行,但对于非并发的operation,通常由operation queue提供线程,这时候,queue拥有该线程,而你的应用不应该动用这个线程;
特别是不要关联任何数据到不是你创建和拥有的线程;
这些线程有queue管理,根据系统和应用的需求创建或销毁,因此使用Per-Thread storage在operations之间传递数据是不可靠的;
对于operation对象,你完全没有理由使用Per-Thread Storage,应该在创建对象的时候就给它需要的所有数据;所有输入和输出数据都应该存储在operation对象中,最后在整合到你的应用,或者最终释放掉;
4)根据需要保留operation对象的引用:
由于operation对象异步执行,你不能创建完以后完全不管,他们也是对象,需要你分配和释放他们管理的任何资源,特别是如果你需要在operation对象完成后获取其中的数据;
对于在queue中执行完的operation可能已经从queue中删除了,因此你总是应该自己拥有operation对象的引用,用以像queue请求operation对象的状态;
5)处理错误和异常:
operation本质上是应用中独立的实体,因此需要自己负责处理所有的错误和异常;
NSOperation默认的start方法并没有捕获异常;所有自己的代码总是要捕获并抑制异常;
如果你覆盖了start方法,你必须捕获所有的异常,阻止它离开底层线程的范围;
需要准备好处理以下错误和异常:
(1)检查并处理UNIX error风格的错误代码;
(2)检查方法或函数显式返回的错误代码;
(3)捕获你的代码或系统frameworks抛出的异常;
(4)捕获NSOperation类自己抛出的异常;
在以下情况下NSOperation回抛出异常:
(1)operation没有准备好,但是调用了start方法;
(2)operation正在执行或已经完成(可能被取消),再次调用了start方法;
(3)当你添加completion block到正在执行或已经完成的operation;
(4)当你试图获取已经取消NSInvocationOperation 对象的结果;
12.为Operation对象确定一个适当的范围:
如果Operation对象只做很少工作,却创建了成千上万个小的Operation对象,时间就更多的花在了调度Operations而不是执行他们;
要高效的使用Operations,关键是在Operation执行的工作量和保持计算机繁忙之间,找到平衡;
同样要避免向一个queue中添加过多的operations,或者快速的像queue中添加operation,超过queue所能处理的能力;这里可以考虑分批创建operations对象,在一批对象执行完之后,使用completion block 告诉应用创建下一批operations对象;
13.执行Operations:
有几种方法执行Operations对象;
1)添加Operations到Operation Queue:
执行Operations最简答的方法是添加到operation queue,后者是NSOperationQueue对象;
应用负责和创建自己使用的所有NSOperationQueue对象;
NSOperationQueue * queue = [[NSOperationQueue alloc] init];
operations添加到queue后,通常短时间内就会得到运行;但是如果存在依赖,或者Operations挂起等原因,也可能需要等待;
注意Operations添加到queue之后,绝对不要修改Operations对象;只能通过NSOperation的方法来查看操作的状态;
通过setMaxConcurrentOperationCount:方法可以强制queue一次只能执行一个Operation;
串行化的Operation queue并不等同于GCD中串行dispatch queue,因为执行两者在执行顺序的依赖因素上不同;
2)手动执行Operations:
手动执行Operation,要求Operation已经准备好,isReady返货yes,此时才能调用start执行;isReady与Operations依赖是结合在一起的;
调用start而不是main来手动执行Operation,因为start在执行你的自定义代码之前,会首先执行一些安全检查;
而且start还会产生KVO通知,以正确的支持Operations的依赖机制;start还能处理Operations已经取消的情况,此时会抛出一个异常;
手动执行Operation对象之前,还需要调用isConcurrent方法,如果返回NO,你的代码可以决定在当前线程同步执行这个Operation,或者创建一个独立的线程以异步执行;
演示下-;//依赖于自定义并发Operation,仍然有问题;
3)取消Operations:
一旦添加Operation queue,queue就拥有了这个对象并且不能被删除,唯一能做的就是取消;
Operation对象可以取消单个操作,queue的cancelAllOperations方法可以取消当前queue中的所有操作;
Operations对象被置为Canceled状态,会阻止它被执行;由于取消也被认为是完成,依赖于它的其它Operations对象会收到适合的KVO通知,并清除依赖状态,然后得到执行;
因此常见做法是当发生重大事件时,一次性取消queue中所有操作,例如应用退出或用户请求取消操作;
4)等待Operations完成:
为了性能,你应该尽量设计你的应用尽可能的异步操作,让应用在操作正在执行时,可以去处理其他事情;
如果创建Operation的代码需要处理Operation完成后的结果,可以使用NSOperation的waitUntilFinished方法等待Operation完成;(通常避免这样做,因为阻止当前线程限制了并发性,引入了更多的串行代码)
queue的waitUntilAllOperationsAreFinished方法,可用于等待queue中的所有操作;在等待一个queue时,应用的其他线程仍然可以往queue中添加Operation,因此可能加长你线程的等待时间;
attention:
绝对不要在应用主线程中等待一个Operation,只能在第二或次要线程中等待;阻止主线程将导致应用无法响应用户事件,应用表现为无响应;
5)挂起和继续Queue:
如果想临时挂起Operations的执行, 可以使用setSuspended:方法暂停相应的queue;不过挂起一个queue不会导致正在执行的Operation在任务中途暂停,只是简单地阻止调度新的Operation执行;
你可以在响应用户请求时,挂起一个queue,来暂停等待中的任务;稍后再根据用户的请求,可以再次调用setSuspended:方法继续Queue中操作的执行;
总结:
在通过start方法启动Operation要求异步执行时,实践上还有些问题,后续资料查询待解决之后,我会在更新下,其他的都比较简单,简单实践下即可;