2

我正在使用 GCD 做一些繁重的工作——图像处理等等——通常同时运行 3 或 4 个任务。

其中一些任务比其他任务完成得更快。如何确保以正确的原始顺序触发回调 -不使用串行队列

例如:

  • 任务一需要 1 秒
  • 任务二需要 5 秒
  • 任务三需要 2 秒

尽管计算时间不同,如何确保最终回调顺序为一、二、三?

// self.queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(self.queue, ^{
    // Long-running code here of varying complexity

    dispatch_async(dispatch_get_main_queue(), ^{
        // Callback here
    });
});

编辑:

根据评论,第一个通知应该在任务一完成后立即发出,即使剩余的任务正在处理中。当任务三完成时,它应该一直保持到任务二完成,然后首先快速连续地通知二和三。

我在想某种用于推动和转移任务的可变数组可能会起作用。有没有更清洁的方法?

4

6 回答 6

12

您的每个完成块(第一个除外)都有两个依赖项:繁重的工作和之前的繁重工作的完成块。

NSOperationQueue使用而NSBlockOperation不是直接使用 GCD来满足您的要求会简单得多。(NSOperationQueue建立在 GCD 之上。)

您需要一个操作队列和对先前完成操作的引用:

@property (nonatomic, strong) NSOperationQueue *queue;
@property (nonatomic, strong) NSOperation *priorCompletionOperation;

将 初始化queueNSOperationQueue. 离开priorCompletionOperationnil 直到你得到第一份工作。

然后只需在将操作提交到队列之前设置依赖项即可:

    NSBlockOperation *heavyLifting = [NSBlockOperation blockOperationWithBlock:^{
        // long-running code here of varying complexity
    }];

    NSBlockOperation *completion = [NSBlockOperation blockOperationWithBlock:^{
        // Callback here
    }];

    [completion addDependency:heavyLifting];
    if (self.priorCompletionOperation) {
        [completion addDependency:self.priorCompletionOperation];
    }

    [self.queue addOperation:heavyLifting];
    [[NSOperationQueue mainQueue] addOperation:completion];

    self.priorCompletionOperation = completion;

请注意,您应确保此作业排队代码一次仅从单个线程运行。如果您只将作业从主线程(或主队列)排入队列,这将自动发生。

于 2013-07-16T09:16:49.427 回答
5

我很欣赏 rob mayoff 的解决方案。然而,也有一种简单的方法可以完全在 GCD 中完成此操作;)

这是一个完整的示例:

#import <Foundation/Foundation.h>
#include <dispatch/dispatch.h>

typedef void (^completion_block_t)(id result);
typedef void (^operation_t)(completion_block_t completionHandler);

static void enqueueOperation(dispatch_queue_t process_queue,
                             dispatch_queue_t sync_queue,
                             operation_t operation)
{
    __block id blockResult = nil;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);

    dispatch_async(sync_queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        NSLog(@"%@", blockResult);
    });

    dispatch_async(process_queue, ^{
        operation(^(id result) {
            blockResult = result;
            dispatch_semaphore_signal(sem);
            // release semaphore
        });
    });
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {

        NSLog(@"Start");

        dispatch_queue_t process_queue = dispatch_get_global_queue(0, 0);
        dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", NULL);

        enqueueOperation(process_queue, sync_queue, ^(completion_block_t completionHandler) {
            sleep(1);
            completionHandler(@"#1");
        });

        enqueueOperation(process_queue, sync_queue, ^(completion_block_t completionHandler) {
            sleep(5);
            completionHandler(@"#2");
        });

        enqueueOperation(process_queue, sync_queue, ^(completion_block_t completionHandler) {
            sleep(2);
            completionHandler(@"#3");
        });

        sleep(6);
        NSLog(@"Finished");
    }
    return 0;
}



2013-07-16 14:16:53.461 test[14140:303] Start
2013-07-16 14:16:54.466 test[14140:1a03] #1
2013-07-16 14:16:58.467 test[14140:1a03] #2
2013-07-16 14:16:58.468 test[14140:1a03] #3
2013-07-16 14:16:59.467 test[14140:303] Finished

(顺便说一下,这是/曾经是我的网络库的一部分,用于并行处理 multipart/x-replace 消息)

于 2013-07-16T12:24:43.077 回答
4

所以最终的解决方案很简单。

我喜欢 NSOperation 的简单性,但我不喜欢必须保留对前一个迭代队列的引用,或者在如此紧密的循环中创建和销毁 NSOperation 的想法。

我最终选择了 GCD/Semaphore 模型的变体:

// self.serialQueue = dispatch_queue_create("com.example.queue", NULL);
// self.conQueue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_CONCURRENT);

// Create a variable that both blocks can access, so we can pass data between them
__block id *message = nil;

// Create semaphore which will be used to lock the serial/callback queue
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

// This is effectively the 'callback' that gets fired when the semaphore is released
dispatch_async(self.serialQueue, ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    if (message) {
        NSLog(@"Callback message: %@", message);
    }
});

// Do the heavy lifting, then release the semaphore which allows the callback to fire
dispatch_async(self.conQueue, ^{

    // ... Heavy lifting here

   message = [NSString stringWithFormat:@"Success!"];
   dispatch_semaphore_signal(semaphore);
});
于 2013-07-17T10:45:49.203 回答
1

你有两个不同的东西,一组并发的图像处理和一组串行的上传处理。您可以创建NSOperations 来处理上传处理并设置它们之间的依赖关系,以便它们必须按顺序运行。这些操作在启动(信号量)后也会等待,直到它们收到要上传的数据。该数据将由并发的 GCD 任务提供。

于 2013-07-16T09:03:06.467 回答
1

如评论中所述,最简单的解决方案是仅在最终任务完成时运行回调。然后,您可以按顺序调用所有任务的回调代码。即,如果需要,将对象添加到数组中,然后当回调中的增量计数器达到数组中的对象数时,执行回调代码。

如果回调是独立于对象的,那么您可以简单地使用计数器并在完成后运行代码。请记住在主线程中或使用“@sync()”指令运行任何计数器操作以避免竞争条件。

dispatch_async(dispatch_get_main_queue(), ^(void){ /* code*/ });

编辑:在完成处理程序中使用相同的数组技术,您将对象的标志设置为准备发送。然后尽可能遍历数组,依次发送所有准备好的对象。否则停止并等待下一个完成处理程序调用。您可以使用计数器来跟踪位置或在完成后从数组中删除项目,但请务必在主线程上或使用同步块进行。

@interface MyImage : NSImage
@property (assign) BOOL ready;
@end


@implementation MyImage

@synthesize ready;

- (void)send {
    //Send image, make sure it's threaded send, NSConnection should be okay
}
@end


 NSArray *imagesNeededToSend = [NSMutableArray arrayWithObjects:image1, image2, image3, nil];

dispatch_async(self.queue, ^{
    // Long-running code here of varying complexity

    dispatch_async(dispatch_get_main_queue(), ^{
        self.ready = YES;
        dispatch_async(dispatch_get_main_queue(), ^(void){ [self sendImages] });
    });
});

...

- (void)sendImages {
    MyImage *firstImage = [imagesNeededToSend firstObject]; //Xcode Beta, but you should have a a category for firstObject, very useful.
   if (firstImage.ready) {
       [firstImage send];
       [imagesNeededToSend removeObjectAtIndex:0];
       [self sendImages];
   }
}
于 2013-07-16T08:52:14.847 回答
1

如何确保以正确的原始顺序触发回调 - 不使用串行队列?

你不知道 - 回调会告诉你任务何时完成,如果它们以与排队顺序不同的顺序完成,那么回调将按照它们的完成顺序触发。

但是,不同之处在于您如何处理接收完成通知。您可以像处理线程问题一样对待它,join在三个工作线程上进行处理。等待第一个,然后是第二个,然后是第三个完成。为此,您可以使用信号量或互斥体(甚至是原子计数器),它们在每个完成时都会引发。

这样,您的处理程序代码并不真正关心完成顺序;它只是等到每个任务按顺序完成。如果任务已经完成,则无需等待。在等待之间,您也可以随时做其他事情。

于 2013-07-16T08:44:55.797 回答