Ожидание завершения нескольких блоков

У меня есть эти методы для получения некоторой информации об объекте из Интернета:

- (void)downloadAppInfo:(void(^)())success
                failure:(void(^)(NSError *error))failure;
- (void)getAvailableHosts:(void(^)())success
                  failure:(void(^)(NSError *error))failure;
- (void)getAvailableServices:(void(^)())success
                     failure:(void(^)(NSError *error))failure;
- (void)getAvailableActions:(void(^)())success
                    failure:(void(^)(NSError *error))failure;

Загруженный материал сохраняется в свойствах объекта, поэтому функции успеха ничего не возвращают.

Теперь я хочу иметь один метод, подобный этому:

- (void)syncEverything:(void(^)())success
               failure:(void(^)(NSError *error))failure;

Который не делает ничего, кроме вызова всех методов выше и возврата только после того, как каждый метод выполнил свой блок успеха или сбоя.

Как я могу это сделать?

Подсказка: я знаю, что каскадные вызовы методов в каждом успешном блоке будут работать. Но это не "чисто" и не полезно, когда более поздние реализации включают дополнительные методы.

Попытки:

Я попытался NSOperation каждый из вызовов в NSOperation и добавить эти NSOperations в NSOperationQueue а затем "операцию завершения", которая зависит от каждой из предыдущих операций.

Это не сработает. Поскольку операции считаются завершенными еще до того, как возвращаются их блоки успеха/неудачи.

Я также пытался использовать dispatch_group. Но мне не ясно, правильно ли я это делаю. К сожалению, это не работает.

Ответ 1

Вы были почти там, проблема, скорее всего, в том, что эти методы асинхронны, поэтому вам нужен дополнительный шаг синхронизации. Просто попробуйте со следующим исправлением:

for(Appliance *appliance in _mutAppliances) {
  dispatch_group_async(
     group,
     dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       dispatch_semaphore_t sem = dispatch_semaphore_create( 0 );

       NSLog(@"Block START");

       [appliance downloadAppInfo:^{
          NSLog(@"Block SUCCESS");
            dispatch_semaphore_signal(sem);
       }
       failure:^(NSError *error){
         NSLog(@"Block FAILURE");
         dispatch_semaphore_signal(sem);
       }];

       dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

       NSLog(@"Block END");
 });

 dispatch_group_notify(
   group,
   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{
     NSLog(@"FINAL block");
     success();
 });
}

Ответ 2

На основе комментариев в других ответах здесь и сообщения в блоге Используя группы рассылки для ожидания нескольких веб-сервисов, я пришел к следующему ответу.

Это решение использует dispatch_group_enter и dispatch_group_leave чтобы определить, когда выполняется каждая промежуточная задача. Когда все задачи завершены, вызывается последний блок dispatch_group_notify. Затем вы можете вызвать свой блок завершения, зная, что все промежуточные задачи завершены.

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);
[self yourBlockTaskWithCompletion:^(NSString *blockString) {

    // ...

    dispatch_group_leave(group);
}];

dispatch_group_enter(group);
[self yourBlockTaskWithCompletion:^(NSString *blockString) {

    // ...

    dispatch_group_leave(group);
}];

dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{

    // All group blocks have now completed

    if (completion) {
        completion();
    }
});

Grand Central Dispatch - Группы отправки

https://developer.apple.com/documentation/dispatch/dispatchgroup

Группировка блоков позволяет осуществлять совокупную синхронизацию. Ваше приложение может отправлять несколько блоков и отслеживать, когда они все завершатся, даже если они могут выполняться в разных очередях. Такое поведение может быть полезно, когда невозможно добиться прогресса, пока все указанные задачи не будут выполнены.

Фрагмент кода Xcode:

Я обнаружил, что использую группы рассылки достаточно, чтобы добавить следующий код в качестве фрагмента кода Xcode для легкой вставки в мой код.

Теперь я DISPATCH_SET и следующий код вставляется. Затем вы копируете и вставляете enter/leave для каждого из ваших асинхронных блоков.

dispatch_group_t group = dispatch_group_create();

dispatch_group_enter(group);

dispatch_group_leave(group);

dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),^{

});

Ответ 3

Еще одно решение - использовать Promise, доступный в нескольких сторонних библиотеках. Я автор RXPromise, который реализует Promises/A +.

Но есть как минимум две другие реализации Objective-C.

A Promise представляет собой конечный результат асинхронного метода или операции:

-(Promise*) doSomethingAsync;

Обещание является полной заменой обработчику завершения. Кроме того, благодаря своей четкой спецификации и дизайну подкладки, он обладает некоторыми очень полезными функциями, которые делают его особенно удобным для решения довольно сложных асинхронных задач.

Что вам нужно сделать в первую очередь, это объединить ваши асинхронные методы с обработчиками завершения в асинхронные методы, возвращающие Promise: (Целесообразно, ваши методы возвращают конечный результат и потенциальную ошибку в более удобном обработчике завершения)

Например:

- (RXPromise*) downloadAppInfo {
    RXPromise* promise = [RXPromise new];
    [self downloadAppInfoWithCompletion:^(id result, NSError *error) {
        if (error) {
            [promise rejectWithReason:error];
        } 
        else {
            [promise fulfillWithValue:result];
        }
    }];
    return promise;
}

Здесь исходный асинхронный метод становится "решателем" обещания. Обещание может быть выполнено (успешно) или отклонено (сбой), либо указав конечный результат задачи, либо причину сбоя. После этого обещание будет содержать конечный результат асинхронной операции или метода.

Обратите внимание, что оболочка представляет собой асинхронный метод, который сразу же возвращает обещание в состоянии ожидания.

Наконец, вы получите конечный результат путем "регистрации" успеха и обработчика отказа с помощью метода или свойства then. Немногие обе библиотеки-библиотеки немного отличаются друг от друга, но в основном это может выглядеть следующим образом:

`promise.then( <success-handler>, <error-handler> )`

Спецификация Promise/A + имеет минималистический API. И выше всего в основном ВСЕ нужны для реализации спецификации Promise/A + - и часто бывает достаточно во многих простых случаях использования.

Однако иногда вам нужно немного больше - например, проблема OPs, которая требует "подождать" по набору асинхронных методов, а затем делать что-то, когда все завершено.

К счастью, Promise - идеальный базовый строительный блок для создания более сложных вспомогательных методов.

Многие библиотеки Promise предоставляют полезные методы. Например, метод all (или аналогичный), который является асинхронным методом, возвращающим Promise и принимающим массив promises в качестве входного. Возвращаемое обещание будет разрешено, когда все операции будут завершены или когда один из них не будет выполнен. Он может выглядеть следующим образом:

Сначала создадим массив из promises и одновременно параллельно запускаем все асинхронные задачи:

NSArray* tasks = @[
    [self downloadAppInfo],
    [self getAvailableHosts],
    [self getAvailableServices],
    [self getAvailableActions],
];

Примечание: здесь задачи уже запущены (и могут завершаться)!

Теперь используйте вспомогательный метод, который делает именно то, что указано выше:

RXPromise* finalPromise = [RXPromise all:tasks];

Получите окончательные результаты:

finalPromise.then(^id( results){
    [self doSomethingWithAppInfo:results[0] 
                  availableHosts:results[1] 
               availableServices:results[2]  
                availableActions:results[3]];
    return nil;
},  ^id(NSError* error) {
    NSLog(@"Error %@", error); // some async task failed - log the error
});

Обратите внимание, что либо вызов, либо обработчик отказа будут вызваны, когда возвращаемое обещание будет каким-то образом разрешено в методе all:.

Возвращаемое обещание (finalPromise) будет разрешено, когда

  • все задачи успешно выполнены или когда
  • Не удалось выполнить одну задачу.

В случае 1) окончательное обещание будет разрешено с помощью массива, который содержит результат для каждой соответствующей асинхронной задачи.

В случае 2) окончательное обещание будет разрешено с ошибкой асинхронной задачи.

(Примечание: несколько доступных библиотек могут отличаться здесь)

Библиотека RXPromise имеет некоторые дополнительные функции:

Сложная аннулирование, которая пересылает сигнал отмены на ациклическом графе promises.

Способ указания очереди отправки, в которой будет выполняться обработчик. Очередь может использоваться для синхронизации доступа к общим ресурсам, например, например.

self.usersPromise = [self fetchUsers];

self.usersPromise.thenOn(dispatch_get_main_queue(), ^id(id users) {
    self.users = users;
    [self.tableView reloadData];
}, nil);

По сравнению с другими подходами решение dispatch_group страдает тем, что он блокирует поток. Это не совсем "асинхронно". Это также довольно сложно, если не невозможно осуществить отмену.

Решение NSOperation представляется смешанным благословением. Он может быть изящным, только если у вас уже есть NSOperations, и если у вас нет обработчиков завершения, которые необходимо учитывать при определении зависимостей, в противном случае он становится загроможденным и разработанным.

Другим решением, не упомянутым до сих пор, является Reactive Cocoa, IMHO, это потрясающая библиотека, которая позволяет решать асинхронные проблемы практически любой сложности. Тем не менее, он имеет довольно крутую кривую обучения и может добавить много кода в ваше приложение. И, я думаю, 90% асинхронных проблем, которые вы натыкаетесь, можно решить с помощью отмены promises. Если у вас еще более сложные проблемы, обратитесь к RAC.

Ответ 4

Если вы хотите создать решение на основе блоков, вы можете сделать что-то вроде

- (void)syncEverything:(void(^)())success failure:(void(^)(NSError *error))failure
{
    __block int numBlocks = 4;
    __block BOOL alreadyFailed = NO;

    void (^subSuccess)(void) = ^(){
        numBlocks-=1;
        if ( numBlocks==0 ) {
            success();
        }
    };
    void (^subFailure)(NSError*) = ^(NSError* error){
        if ( !alreadyFailed ) {
            alreadyFailed = YES;
            failure(error);
        }
    };

    [self downloadAppInfo:subSuccess failure:subFailure];
    [self getAvailableHosts:subSuccess failure:subFailure];
    [self getAvailableServices:subSuccess failure:subFailure];
    [self getAvailableActions:subSuccess failure:subFailure];
}

Это быстро и грязно, и вам, возможно, придется делать блок-копии. Если несколько методов не работают, вы получите только один общий сбой.

Ответ 5

Вот мое решение без каких-либо dispatch_group.

+(void)doStuffWithCompletion:(void (^)(void))completion{
    __block NSInteger stuffRemaining = 3;

    void (^dataCompletionBlock)(void) = ^void(void) {
        stuffRemaining--;

        if (!stuffRemaining) {
            completion();
        }
    };

    for (NSInteger i = stuffRemaining-1; i > 0; i--) {
        [self doOtherStuffWithParams:nil completion:^() {
            dataCompletionBlock();
        }];
    }
}