I'm a big fan of blocks, but have not used them for concurrency. After some googling, I pieced together this idea to hide everything I learned in one place. The goal is to execute a block in the background, and when it's finished, execute another block (like UIView animation)...
- (NSOperation *)executeBlock:(void (^)(void))block completion:(void (^)(BOOL finished))completion {
NSOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:block];
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
completion(blockOperation.isFinished);
}];
[completionOperation addDependency:blockOperation];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
NSOperationQueue *backgroundOperationQueue = [[NSOperationQueue alloc] init];
[backgroundOperationQueue addOperation:blockOperation];
return blockOperation;
}
- (void)testIt {
NSMutableString *string = [NSMutableString stringWithString:@"tea"];
NSString *otherString = @"for";
NSOperation *operation = [self executeBlock:^{
NSString *yetAnother = @"two";
[string appendFormat:@" %@ %@", otherString, yetAnother];
} completion:^(BOOL finished) {
// this logs "tea for two"
NSLog(@"%@", string);
}];
NSLog(@"keep this operation so we can cancel it: %@", operation);
}
My questions are:
- It works when I run it, but am I missing anything ... hidden land mine? I haven't tested cancellation (because I haven't invented a long operation), but does that look like it will work?
- I'm concerned that I need to qualify my declaration of backgroundOperation so that I can refer to it in the completion block. The compiler doesn't complain, but is there a retain cycle lurking there?
- If the "string" were an ivar, what would happen if I key-value observed it while the block was running? Or setup a timer on the main thread and periodically logged it? Would I be able to see progress? Would I declare it atomic?
- If this works as I expect, then it seems like a good way to hide all the details and get concurrency. Why didn't Apple write this for me? Am I missing something important?
Thanks.
There's no need to set up a block to be run on completion and add dependencies like this.
NSBlockOperation
like allNSOperation
subclasses already has acompletionBlock
property which will automatically run when the block has finished its work:The completion block is run when its block moves to the
finished
state.I am not an expert in NSOperation or NSOperationQueues but I think below code is a bit better although I think it has some caveats still. Probably enough for some purposes but is not a general solution for concurrency:
Now lets use it:
Some answers to your questions:
Cancelation. Usually you subclass NSOperation so you can check
self.isCancelled
and return earlier. See this thread, it is a good example. In current example you cannot check if the operation has being cancelled from the block you are supplying to make anNSBlockOperation
because at that time there is no such an operation yet. CancellingNSBlockOperation
s while the block is being invoked is apparently possible but cumbersome.NSBlockOperation
s are for specific easy cases. If you need cancellation you are better subclassingNSOperation
:)I don't see a problem here. Although note two things. a)I changed the method do to run the completion block in current queue b)a queue is required as a parameter. As @Mike Weller said, you should better supply
background queue
so you don't need to create one per each operation and can choose what queue to use to run your stuff :)I think yes, you should make
string
atomic
. One thing you should not forget is that if you supply several operations to the queue they might not run in that order (necessarily) so you could end up with a very strange message in yourstring
. If you need to run one operation at a time serially you can do:[backgroundOperation setMaxConcurrentOperationCount:1];
before start enqueuing your operations. There is a reading-worthy note in the docs though:I think after reading these lines you know :)
You should not be creating a new
NSOperationQueue
for eachexecuteBlock:completion:
call. This is expensive and the user of this API has no control over how many operations can execute at a time.If you are returning
NSOperation
instances then you should leave it up to the caller to decide which queue to add them to. But at that point, your method really isn't doing anything helpful and the caller might as well create theNSBlockOperation
themselves.If you just want a simple and easy way to spin off a block in the background and perform some code when it finishes, you're probably better off making some simple GCD calls with the
dispatch_*
functions. For example: