Objective-C等待异步操作并排队完成处理程序

4

我需要计算一个代价高昂的值。在计算完成后,我想运行一个完成处理程序块:

-(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);
        });
    });
}

非常标准。

现在,我希望能够重复调用此函数,而不需要重新排队costlyCalculation。如果costlyCalculation已经在运行,我想只保存完成块,并在costlyCalculation完成后使用相同的result调用它们所有。

是否可以使用GCD或NSOperationQueue简单实现这一点?或者我应该将完成块存储在一个NSArray中并自己调用它们?如果我这样做,我需要在这个数组周围放置什么样的同步?

更新

使用dispatch_group_notify,我可以接近解决问题。基本上,我可以将工作块排队,并将所有完成处理程序排队以在组后运行:

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);
});

这个方案可行,但如果我想知道costlyCalcuation是否已经在运行,且不需要排队工作,我该怎么办?

不想事先检查 result,因为在第一次运行时会有多个请求快速连续到达,这将导致对 costlyCalculation 的重复调用。也许用 BOOLs 标记开始和结束是正确的方法。感谢您的建议。 - bcattle
我同意@Rob的观点,但我也要提到你可以使用BOOL方法和iVar方法相结合来防止多次调用costlyCalculation,然后保留结果(如果结果始终相同,则保留结果比每次计算更可取)。 - Rob Sanders
我使用generateCGImagesAsynchronouslyForTimes方法生成图片,这些图片需要在屏幕上的不同位置的多个不同图层中重复使用。所以如果在generateCGImagesAsynchronouslyForTimes正在运行时有一个新图层出现,我希望在这个新图层中重复使用相同的图片而不重新运行图像生成器。 - bcattle
generateCGImagesAsynchronouslyForTimes没有缓存的概念,因此如果在函数运行时有多个相同的调用,则将它们包装在一起的层中是合理的。或者我误解了你的意思? - bcattle
我很感激您的帮助,但问题是关于一般并发编程问题,而不是如何有效地使用generateCGImagesAsynchronouslyForTimes - bcattle
显示剩余2条评论
1个回答

1

我认为您已经基本解决了问题。我只是想提供一种使用NSOperationQueue和NSOperations之间的依赖关系的替代方案。以下是我想到的伪代码。

// 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];
    }
}

然而,可能存在微妙的时间问题。也许我们可以在costlyCalculation函数中使用额外的标志。


谢谢你提供代码。你想象中有什么时间问题吗? - bcattle
@bcattle 理论上,在第一个 if 块中创建 NSOperation 对象时,costlyCalculation 可以完成。您创建了新操作,认为有操作可以依赖,但创建后它就消失了。我不确定在这种情况下会发生什么 :) - Wonjae
这段代码假定正在执行的任务本身是同步的。如果它是异步的,你必须将其包装在一个异步的NSOperation子类中,或者更糟糕的是,通过使用信号量或类似的方式使其表现为同步。 - Rob
@Rob NSOperation被添加到队列中是异步运行的,这就是为令我调用addDependency方法使其同步。或者,我是否从您的评论中漏掉了一些要点? - Wonjae
我的观点是,如果你创建了一个调用异步方法(例如generateCGImagesAsynchronouslyForTimes)的NSBlockOperationNSInvocationOperation,即使启动的任务仍在进行中,操作也会立即完成,从而完全破坏了依赖关系的目的。然而,异步的NSOperation子类可以这样实现,即在定义的时候不会发出“完成”的信号(例如,在异步操作的所有部分都完成后发布isFinished KVO)。 - Rob
@Rob 我明白你的意思。是的,我假设了同步的昂贵计算任务,但我认为这就是为什么bcattle在第一次调用dispatch_async块时调用了costlyCalculation的原因。如果costlyCalculation已经在内部依赖于异步调用,那么保持全局标志和NSMutableArray用于完成块就足够了。但我相信情况并非如此。 - Wonjae

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接