Как подождать метода, который имеет блок завершения (все в основном потоке)?

У меня есть следующий (псевдо) код:

- (void)testAbc
{
    [someThing retrieve:@"foo" completion:^
    {
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        for (NSString name in names)
        {
            [someObject lookupName:name completion:^(NSString* urlString)
            {
                // A. Something that takes a few seconds to complete.
            }];

            // B. Need to wait here until A is completed.
        }
    }];

    // C. Need to wait here until all iterations above have finished.
    STAssertTrue(...);
}

Этот код работает на основном потоке, а также блок завершения A находится в основном потоке.

  • Как подождать завершения B для A?
  • Как выполнить следующий этап ожидания для C для завершения внешнего блока завершения?

Ответы

Ответ 1

Если ваш блок завершения также находится в главном потоке, вы не можете достичь этого (см. раздел "Редактирование" ниже), потому что для завершения выполнения ваш метод должен вернуться первым. Вам необходимо изменить реализацию асинхронного метода на:

  • Быть синхронным.
    или
  • Используйте другой поток/очередь для завершения. Затем вы можете использовать Семафоры отправки для ожидания. Вы инициализируете семафор со значением 0, затем вызываете wait в основном потоке и signal в завершение.

В любом случае блокировка Main Thread - очень плохая идея в приложениях GUI, но это не было частью вашего вопроса.


Изменить: Есть способ, но может иметь неожиданные последствия. Будьте осторожны!

Основная тема является специальной. Он работает +[NSRunLoop mainRunLoop], который также обрабатывает +[NSOperationQueue mainQueue] и dispatch_get_main_queue(). Все операции или блоки, отправленные в эти очереди, будут выполняться в пределах цикла основного запуска. Это означает, что методы могут принимать любой подход к планированию блока завершения, это должно работать во всех этих случаях. Вот он:

__block BOOL isRunLoopNested = NO;
__block BOOL isOperationCompleted = NO;
NSLog(@"Start");
[self performOperationWithCompletionOnMainQueue:^{
    NSLog(@"Completed!");
    isOperationCompleted = YES;
    if (isRunLoopNested) {
        CFRunLoopStop(CFRunLoopGetCurrent()); // CFRunLoopRun() returns
    }
}];
if ( ! isOperationCompleted) {
    isRunLoopNested = YES;
    NSLog(@"Waiting...");
    CFRunLoopRun(); // Magic!
    isRunLoopNested = NO;
}
NSLog(@"Continue");

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

Если -performOperationWithCompletionOnMainQueue: асинхронный, выход будет выглядеть следующим образом:

Start
Ожидание...
Завершена!
Продолжить

Если метод синхронный, выход будет выглядеть следующим образом:

Start
Завершена!
Продолжить

Что такое Магия? Вызов CFRunLoopRun() не возвращается сразу, но только при вызове CFRunLoopStop(). Этот код находится в Main RunLoop, поэтому запуск Main RunLoop снова возобновит выполнение всех запланированных блоков, таймеров, сокетов и т.д.

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

Вы можете обернуть эту логику в объект, который упростит повторение этого шаблона:

@interface MYRunLoopSemaphore : NSObject

- (BOOL)wait;
- (BOOL)signal;

@end

Таким образом, код будет упрощен:

MYRunLoopSemaphore *semaphore = [MYRunLoopSemaphore new];
[self performOperationWithCompletionOnMainQueue:^{
    [semaphore signal];
}];
[semaphore wait];

Ответ 2

Я думаю, что Майк Эш (http://www.mikeash.com/pyblog/friday-qa-2013-08-16-lets-build-dispatch-groups.html имеет именно ответ на "ожидание завершения нескольких потоков" и затем сделать что-то, когда все потоки закончены ". Приятно, что вы даже можете ждать синхронно или синхронно, используя группы отправки.

Короткий пример скопировал и модифицировал Майка Эша в блоге:

    dispatch_group_t group = dispatch_group_create();

    for(int i = 0; i < 100; i++)
    {
        dispatch_group_enter(group);
        DoAsyncWorkWithCompletionBlock(^{
            // Async work has been completed, this must be executed on a different thread than the main thread

            dispatch_group_leave(group);
        });
    }

dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

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

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    UpdateUI();
});

Ответ 3

int i = 0;
//the below code goes instead of for loop
NSString *name = [names objectAtIndex:i];

[someObject lookupName:name completion:^(NSString* urlString)
{
    // A. Something that takes a few seconds to complete.
    // B.
    i+= 1;
    [self doSomethingWithObjectInArray:names atIndex:i];


}];




/* add this method to your class */
-(void)doSomethingWithObjectInArray:(NSArray*)names atIndex:(int)i {
    if (i == names.count) {
        // C.
    }
    else {
        NSString *nextName = [names objectAtIndex:i];
        [someObject lookupName:nextName completion:^(NSString* urlString)
        {
            // A. Something that takes a few seconds to complete.
            // B.
            [self doSomethingWithObjectInArray:names atIndex:i+1];
        }];
    }
}

Я просто набрал код здесь, поэтому некоторые имена методов могут быть ошибочными.

Ответ 4

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

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

Он также использует категорию для NSArray, которая не является частью библиотеки, но может быть легко реализована с использованием библиотеки RXPromise.

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

- (RXPromise*)asyncTestAbc
{
    return [someThing retrieve:@"foo"]
    .then(^id(id unused /*names?*/) {
        // retrieve:@"foo" finished with success, now execute this on private queue:
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        return [names rx_serialForEach:^RXPromise* (id name) { /* return eventual result when array finished */
            return [someObject lookupName:name] /* return eventual result of lookup completion handler */
            .thenOn(mainQueue, ^id(id result) {
                assert(<we are on main thread>);
                // A. Do something after a lookupName:name completes a few seconds later
                return nil;
            }, nil /*might be implemented to detect a cancellation and "backward" it to the lookup task */);
        }]
    },nil);
}

Чтобы проверить окончательный результат:

[self asyncTestAbc]
.thenOn(mainQueue, ^id(id result) {
    // C. all `[someObject lookupName:name]` and all the completion handlers for
    // lookupName,  and `[someThing retrieve:@"foo"]` have finished.
    assert(<we are on main thread>);
    STAssertTrue(...);
}, id(NSError* error) {
    assert(<we are on main thread>);
    STFail(@"ERROR: %@", error);
});

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

  [[self asyncTestAbc].thenOn(...) wait];

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


Пожалуйста, запросите более подробное объяснение, если найдете это полезным!


Примечание. Библиотека RXPromise по-прежнему "работает в процессе". Это может помочь всем, кто занимается сложными асинхронными шаблонами. В приведенном выше коде используется функция, которая в настоящий момент не выполняется для master на GitHub: свойство thenOn, где может быть указана очередь, в которой будут выполняться обработчики. В настоящее время существует только свойство then, которое опускает очередь параметров, в которой должен выполняться обработчик. Если не указано иное, весь обработчик запускается в общей частной очереди. Предложения приветствуются!

Ответ 5

Это часто плохой подход, чтобы блокировать основной поток, он просто сделает ваше приложение невосприимчивым, поэтому почему бы не сделать что-то подобное вместо этого?

NSArray *names;
int namesIndex = 0;
- (void)setup {

    // Insert code for adding loading animation

    [UIView animateWithDuration:1 animations:^{
        self.view.alpha = self.view.alpha==1?0:1;
    } completion:^(BOOL finished) {
        names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        [self alterNames];
    }];
}

- (void)alterNames {

    if (namesIndex>=names.count) {
        // Insert code for removing loading animation
        // C. Need to wait here until all iterations above have finished.
        return;
    }


    NSString *name = [names objectAtIndex:namesIndex];
    [UIView animateWithDuration:1 animations:^{
        self.view.alpha = self.view.alpha==1?0:1;
    } completion:^(BOOL finished) {
        name = @"saf";
        // A. Something that takes a few seconds to complete.
        // B. Need to wait here until A is completed.

        namesIndex++;
        [self alterNames];
    }];

}

Я только что использовал [анимацию UIView...], чтобы сделать, к примеру, полностью функциональным. Просто скопируйте и вставьте в свой viewcontroller.m и вызовите [self setup]; Конечно, вы должны заменить это кодом.

Или если вы хотите:

NSArray *names;
int namesIndex = 0;
- (void)setup {

    // Code for adding loading animation

    [someThing retrieve:@"foo" completion:^ {
        names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        [self alterNames];
    }];
}

- (void)alterNames {

    if (namesIndex>=names.count) {
        // Code for removing loading animation
        // C. Need to wait here until all iterations above have finished.
        return;
    }

    NSString *name = [names objectAtIndex:namesIndex];
    [someObject lookupName:name completion:^(NSString* urlString) {
        name = @"saf";
        // A. Something that takes a few seconds to complete.
        // B. Need to wait here until A is completed.

        namesIndex++;
        [self alterNames];
    }];

}

Пояснение:

  • Запустите все, позвонив по [self setup];
  • Вызывается блок, когда someThing извлекает "foo", другими словами, он будет ждать, пока someThing не получит "foo" (и основной поток не будет заблокирован).
  • Когда блок выполняется, alterNames вызывается
  • Если все элементы в "именах" зацикливаются, "цикл" остановится, и C может быть выполнен.
  • Ищите, найдите имя и , когда это сделаете, сделайте что-нибудь с ним (A), и потому, что это происходит в основном потоке (вы ничего не сказали), вы могли бы сделать B тоже.
  • Итак, , когда A и B завершены, вернитесь к 3

См?

Удачи вам в вашем проекте!

Ответ 6

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

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

- (void)testAbc
{
    [someThing retrieve:@"foo" completion:^
    {
        STSuccess();
    }];

    STFailAfter(500, @"block should have been called");
}

Я бы также рекомендовал тестировать someThing и someObject в двух отдельных тестах, но это независимо от асинхронного характера тестируемого.

Ответ 7

 Move B and C to two methods.

int flagForC = 0, flagForB = 0;
     [someThing retrieve:@"foo" completion:^
    {
        flagForC++;
        NSArray* names = @[@"John", @"Mary", @"Peter", @"Madalena"];
        for (NSString name in names)
        {
            [someObject lookupName:name completion:^(NSString* urlString)
            {
                // A. Something that takes a few seconds to complete.
               flagForB++;

               if (flagForB == [names Count])
               {
                   flagForB = 0;
                   //call B
                    if (flagForC == thresholdCount)
                    {
                          flagForC = 0;
                         //Call C 
                    }
               }
            }];


        }
    }];