4

I have to calculate a costly value. After this value is computed, I'd like to run a completion handler block:

-(void) performCostlyCalculationWithCompletionHandler:(void (^)(void)complete 
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        id result = [self costlyCalculation];
        dispatch_async(dispatch_get_main_queue(), ^{
            complete(result);
        });
    });
}

Pretty standard.

Now, I'd like to be able to call this function repeatedly, without re-enqueueing costlyCalculation. If costlyCalculation is already running I'd like to just save the completion blocks and call them all with the same result once costlyCalculation finishes.

Is there a simple way to do this with GCD or an NSOperationQueue? Or should I just store the completion blocks in an NSArray and call them myself? If I do this, what sort of synchronization do I need to put around this array?

UPDATE

I'm able to get close with dispatch_group_notify. Basically, I can enqueue the work blocks and enqueue all completion handlers to run after the group:

dispatch_group_t group = dispatch_group_create();

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^(){
    // Do something that takes a while
    id result = [self costlyCalculation];
    dispatch_group_async(group, dispatch_get_main_queue(), ^(){
        self.result = result;
    });
});

dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
    complete(result);
});

This would work, but how can I tell if costlyCalcuation is already running and not enqueue the work if I don't need to?

bcattle
  • 12,115
  • 6
  • 62
  • 82
  • Could you either save the result as an ivar and then test for the existence of `result` before performing `costlyCalculation`, or simply mark the start and end of the function using BOOLs and then if the function is running add the block to an array, as mentioned in your q and call all queued blocks once the calculation completes. I've done both of those things before if you want some code. – Rob Sanders Mar 05 '15 at 00:01
  • Don't want to check for `result` beforehand, because multiple requests will come in rapid succession while it's running the first time, which would result in duplicate calls to `costlyCalculation`. Maybe marking the start and end with BOOLs is the right way to go. Thanks for the suggestion. – bcattle Mar 05 '15 at 00:07
  • Sure, I'm using an `AVAssetImageGenerator` to generate thumbnails from an asset. The thumbnails then are drawn into several `CALayers` in different locations on the screen. Visible layers may come and go while the `AVAssetImageGenerator` runs. – bcattle Mar 05 '15 at 00:10
  • I agree with @Rob but I will also mention that you could use the BOOL method and the iVar method combined to prevent multiple calls to the `costlyCalculation` and then keep the result hanging around (which would be preferable to working it out each time if the result is always the same). – Rob Sanders Mar 05 '15 at 00:10
  • I call `generateCGImagesAsynchronouslyForTimes`, with the resulting images needing to be re-used in several different layers in different locations on the screen. So if a new layer appears while `generateCGImagesAsynchronouslyForTimes` is running, I want to re-use the same images in this new layer without re-running the image generator – bcattle Mar 05 '15 at 01:52
  • `generateCGImagesAsynchronouslyForTimes` has no notion of caching, therefore it is reasonable to wrap it in a layer that stacks several identical calls together if they come in while the function is running. Or am I misunderstanding your point? – bcattle Mar 05 '15 at 02:17
  • I appreciate the help, but the question is about a general concurrent programming issue, not how to efficiently use `generateCGImagesAsynchronouslyForTimes` – bcattle Mar 05 '15 at 04:23

1 Answers1

1

I think you've mostly solved the problem already. I just came up with an alternative using NSOperationQueue and dependency between NSOperations. Here's a pseudo code I think.

// somewhere, create operation queue
NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];

-(void)tryCalculation:(CompletionBlockType)completionBlock
{
    if(opQueue.operationCount > 0)
    {
        NSOperation *op = [[NSOperation alloc] init];
        op.completionBlock = completionBlock;
        // you can control how to synchronize completion blocks by changing dependency object. In this example, all operation will be triggered at once when costly calculation finishes
        [op addDependency:[opQueue.operations firstObject]];
        [opQueue addOperation:op];
    }
    else
    {
        NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(costlyCalculation) object:nil];
        op.completionBlock = completionBlock;
        [opQueue addOperation:op];
    }
}

Still, there can be subtle timing issue. Maybe we could use additional flag in costlyCalculation function.

Wonjae
  • 309
  • 2
  • 6
  • Thanks for the code. What timing issue do you imagine? – bcattle Mar 05 '15 at 01:54
  • @bcattle Theoretically, costlyCalculation can finish while creating NSOperation object in first if block. You created new operation thinking there's operation to depend on, but after creation it's gone. I'm not sure what would happen in such a case :) – Wonjae Mar 05 '15 at 02:06
  • This code presumes that the task being performed is itself synchronous. If it's asynchronous, you have to wrap it an [asynchronous `NSOperation` subclass](https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW8) or, worse, make it behave synchronously through the use of semaphores or the like. – Rob Mar 05 '15 at 06:09
  • @Rob NSOperation added to queue runs asynchronously, and that's why I call addDependency method to make it synchronous. Or, am I missing some points from your comment? – Wonjae Mar 05 '15 at 07:32
  • My point is that if you create a `NSBlockOperation` or `NSInvocationOperation` which calls something that is, itself, asynchronous (e.g. `generateCGImagesAsynchronouslyForTimes` for example), the operation will finish immediately even though the initiated task is still in progress, thus completely defeating the purpose of dependencies. An asynchronous `NSOperation` subclass, though, can be implemented in such a way that the operation doesn't signal that it is "finished" until such point that you define it to (e.g. that you post the `isFinished` KVO when async parts of the op are all done). – Rob Mar 05 '15 at 11:39
  • @Rob Got your point. Yes, I presumed synchronous costlyCalculation task, but I think that was why bcattle called costlyCalculation in dispatch_async block in the first place. If costlyCalculation already depends on a async call internally, keeping global flag and NSMutableArray for completion blocks would have been enough. But I believe that was not the case. – Wonjae Mar 05 '15 at 16:38