Concurrency Programming Guide 3 Operation Queues
Operation Queues
与GCD对比
稍微慢一点点,因为是NSOperation和NSOperationQueue基于GCD的(在Mac OS X v10.6 & iOS 4之后1);
Operation Queue可以实现operation的dependency功能,使之在制定的operation完成之后再运行。而且指定线程可以在不同的operation queue里跑;
可以重用、取消、暂停operation;
可以使用KVO,方便监控operation的运行状态;
Operation既可以直接调用
start
方法独立运行也可以add到operation queue里面跑;可以使用
setCompletionBlock:
设置一个operation执行完之后的动作;可以定义operation优先级别
setQueuePriority:
;
使用Operation
可以使用NSInvocationOperation
、NSBlockOperation
和sub class NSOperation
;
1. NSInvocationOperation
只需要传入obj和method selector就可以,适合用来修改已经成型的代码走并发。
NSInvocationOperation *tmpOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myTaskMethod:) object:obj];
2. NSBlockOperation
使用NSBlockOperation可以直接传入一个或多个Block,加到同一个Operation的所有Blocks将会并发执行。只有在所有Blocks执行完之后这个Operation才会算是执行完了
NSBlockOperation *tmpOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog('log somthing...');
//do sth
}]
之后可以使用addExecutionBlock:
方法来添加额外的Block
3. NSOperation
因为NSOperation是抽象基类,所以需要写子类来实现。
默认isConcurrent
方法返回NO,表示Operation是和calling thread同步运行的。
如果你不想把一个Operation加入到Operation queue,而要手动执行并让它和calling thread异步运行,你需要写额外的代码创建新的线程来执行这个Operation哟。
不管add到Operation queue的Operation是否可以和其他Operation并发执行(可能这个Operation加了依赖于其他Operation完成之后才能运行的dependency),Operation queue最终执行它的时候都会创建一个新的thread来异步运行这个Operation。
Define Custom Operation Object
对于非并行的task,至少需要实现main
和一个自定义的初始化方法。这个自定义的初始化方法是用来把这个Operation设定好,好让main方法执行。
一些你可能需要实现的方法:
- main方法中需要调用的方法
- 访问Operation内部数据/执行结果的Accessor
- NSCoding 协议方法
还有,为了防止Operation在执行过程中意外中断,还需要在运行过程中检查当前Operation是否被cancel(使用Operation的isCancelled
方法)。如果isCancelled
方法返回YES,需要马上做出响应,如保存当前的计算结果和释放创建的资源。
- 因为Operation有可能在执行之前就被cancel了,所以需要在一开始的代码就判断
- 在循环头也需要判断
- 在有可能让Operation aboard出去的地方
一个栗子1:
@interface MyNonConcurrentOperation : NSOperation
@property id (strong) myData;
-(id)initWithData:(id)data;
@end
@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
if (self = [super init])
myData = data;
return self;
}
-(void)main {
@try {
BOOL isDone = NO;
while (![self isCancelled] && !isDone) {
// Do some work on myData and set isDone to YES when finished.
}
}
@catch(...) {
// Do not rethrow exceptions.
} }
@end
对于需要并行执行的Operation,你需要重写override一些方法来让Operation queue里面的Operation异步并行地执行,虽然Operation queue本来就是异步运行Operation的。
方法 | 描述 |
---|---|
start |
(必要)手动运行Operation调用的方法,不能调用其super |
main |
(可选)一般把start 之后运行的主要代码写在这里 |
isExecuting 、isFinished |
(必要)标明当前执行状态,同时还需要负责实现KVO |
isConcurrent |
(必要)如果Operation需要动态地改变并发方式,可以重写该方法。这里重写override返回YES即可 |
栗子2:
@interface MyOperation : NSOperation {
BOOL executing;
BOOL finished;
}
- (void)completeOperation;
@end
@implementation MyOperation
- (id)init {
self = [super init];
if (self) {
executing = NO;
finished = NO;
}
return self;
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
- (void)start {
// Always check for cancellation before launching the task.
if ([self isCancelled])
{
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];
return; }
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:@"isExecuting"];
[NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:@"isExecuting"];
}
@end
下面这个栗子手动加入了完成Operation的任务后执行的代码:
- (void)main {
@try {
// Do the main work of the operation here.
[self completeOperation];
}
@catch(...) {
// Do not rethrow exceptions.
} }
- (void)completeOperation {
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
}
关于isFinished函数
该函数涉及到KVO
因为Operation queue默认是非并行执行的,如果你不返回YES,接下来得Operation将无法执行。如果你有其他Operation是设置了基于它的dependency,那么它也无法执行哟。
KVO
在重写以上函数的时候,你需要维护以下函数KVO:
isCancelled
isConcurrent
isExecuting
isFinished
isReady
dependencies
queuePriority
completionBlock
其中,重写override isReady
还会可以自定义Operation的dependency逻辑。如果你需要保持dependency的基本逻辑,记得调用其super方法。还有就是dependency的KVO key path不需要管,除非你重写了addDependency:
和removeDependency:
方法。
Customizing the Execution Behavior of an Operation Object
以下设置对所有Operation obj有效,不管是你自己写的子类还是NSInvocationOperation、NSBlockOperation。
Configuring Interoperation Dependencies
dependency是用来实现Operation之间的运行依赖关系的,可以是一对一、一对多的依赖。例如说Operation1需要在Operation2和Operation3之后运行。
你可以在把Operation添加到OperationQueue或手动运行Operation之前设置:
NSBlockOperation *op1,*op2,*op3;
[op1 addDependency:op2];
[op1 addDependency:op3];
这里并不要求op1,op2,op3在同一个OperationQueue里面。
注意不要创建循环的dependency。
除非你重写override了isReady方法,否则在op2和op3完成之后,op1的isReady状态就会变成YES。
还有dependency依赖KVO运行,重写相关方法时要注意。
修改Operation的优先级别
不要依靠优先级别来控制Operation之间的dependency,要使用dependency还是得用addDependency:
方法。
使用函数:setQueuePriority:
优先级别:
typedef enum : NSInteger {
NSOperationQueuePriorityVeryLow = -8,
NSOperationQueuePriorityLow = -4,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
} NSOperationQueuePriority;
修改所属Operation Queue的优先级别
setThreadPriority:
参数:0.0-1.0 大的优先级别高
这个优先级别只会影响当前Operation所属的Operation Queue,如果你重写了Operation的start
方法并另外创建了线程来并发task,这个创建的线程将不受这个参数影响。
设置Completion Block
setCompletionBlock:
Tips
内存管理
详情可以参阅《_Advanced Memory Management Programming Guide _》
Avoid Per-Thread Storage
- 在Operation obj中避免直接和线程打交道。
- 创建Operation的时候就应该把task和相关的变量创建好,避免在运行时创建基于线程的变量(以防运行完线程和数据都被释放)。
Keep References to Your Operation Object As Needed
Operation Queue并不负责保存Operation的指针,它只会尽快的执行添加到里面的Operation,运行完之后会remove出去。所以如果你需要在Operation执行完之后取出保存在里面的数据,你需要保存指向Operation的指针避免它被release。
Catch Errors 和 Exceptions
这些情况下,要考虑try-catch:
- Check and handle UNIX errno-style error codes.
- Check explicit error codes returned by methods and functions.
- Catch exceptions thrown by your own code or by other system frameworks.
- Catch exceptions thrown by the NSOperation class itself, which throws exceptions in the following situations:
- When the operation is not ready to execute but its start method is called
- When the operation is executing or finished (possibly because it was canceled) and its start method
is called again
- When the operation is executing or finished (possibly because it was canceled) and its start method
- When you try to add a completion block to an operation that is already executing or finished When you try to
- retrieve the result of an NSInvocationOperation object that was canceled
Determining an Appropriate Scope for Operation Objects
如果有100个task,考虑使用100/4个Operation来执行。
详情参考《Performance Overview》
运行Operation
添加到Operation Queue
一个时间里可以并发的OperationQueue有系统负载和可用内核数有关,不是创建越多OperationQueue就可以并发越多Operation。
Operation Queue执行Operation受优先级别和dependency的影响。
不要修改已经添加到Queue的Operation。
获取Operation的运行状态调用isCancelled
/ isExecuting
/ isFinished
/ isReady
等方法。
使用setMaxConcurrentOperationCount:
可以设置Operation Queue的并发数量。然而即使设置为1,OperationQueue执行Operation的顺序还是受优先级别和dependency的影响哒哟。这是和GCD的serail dispatch queue不同的。
方法:
addOperations:waitUntilFinished:
addOperationWithBlock:
栗子:
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
[aQueue addOperation:anOp]; // Add a single operation
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations [aQueue addOperationWithBlock:^{
NSOperationQueue* aQueue = [[NSOperationQueue alloc] init];
/* Do something. */
}];
手动运行Operation
使用start
方法运行
start
方法会做一些检查工作,如判断是否ready、KVO。
- (BOOL)performOperation:(NSOperation*)anOp
{
BOOL ranIt = NO;
if ([anOp isReady] && ![anOp isCancelled])
{
if (![anOp isConcurrent])
[anOp start];
else
[NSThread detachNewThreadSelector:@selector(start) toTarget:anOp withObject:nil];
ranIt = YES;
}
else if ([anOp isCancelled])
{
// If it was canceled before it was started,
// move the operation to the finished state.
[self willChangeValueForKey:@"isFinished"];
[self willChangeValueForKey:@"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:@"isExecuting"];
[self didChangeValueForKey:@"isFinished"];
// Set ranIt to YES to prevent the operation from
// being passed to this method again in the future.
ranIt = YES;
}
return ranIt;
}
Cancel Operations
对Operation
cancel
对Operation Queue
cancelAllOperations
cancel后的Operation也算是finish
等待 Operation 完成
方法:
waitUntilFinished
挂起调用线程,直到Operation执行完。
waitUntilAllOperationsAreFinished
挂起调用线程,直到Queue中所有Operation执行完。
Suspend、Resume Queues
Operation在接收到Suspend消息后还是会执行完当前正在跑的Operation的。只会在Operation之间Suspend。