Як чекати, коли асинхронно відправлений блок закінчиться?


180

Я тестую деякий код, який робить асинхронну обробку за допомогою Grand Central Dispatch. Тестовий код виглядає приблизно так:

[object runSomeLongOperationAndDo:^{
    STAssert
}];

Тести повинні дочекатися закінчення операції. Моє поточне рішення виглядає приблизно так:

__block BOOL finished = NO;
[object runSomeLongOperationAndDo:^{
    STAssert
    finished = YES;
}];
while (!finished);

Що виглядає трохи грубо, ви знаєте кращий спосіб? Я міг би відкрити чергу, а потім заблокувати, зателефонувавши dispatch_sync:

[object runSomeLongOperationAndDo:^{
    STAssert
}];
dispatch_sync(object.queue, ^{});

... але це, можливо, викриває надто багато object.

Відповіді:


302

Спроба використовувати a dispatch_semaphore. Це має виглядати приблизно так:

dispatch_semaphore_t sema = dispatch_semaphore_create(0);

[object runSomeLongOperationAndDo:^{
    STAssert

    dispatch_semaphore_signal(sema);
}];

if (![NSThread isMainThread]) {
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
} else {
    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) { 
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]]; 
    }
}

Це має поводитися правильно, навіть якщо runSomeLongOperationAndDo:вирішує, що операція насправді не є достатньо довгою, щоб заслуговувати нарізку, а замість цього виконується синхронно.


61
Цей код для мене не працював. Мій STAssert ніколи не виконується. Я повинен був замінити dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);зwhile (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]]; }
nicktmro

41
Це, мабуть, тому, що ваш блок завершення відправляється в основну чергу? Черга блокується в очікуванні семафору і тому ніколи не виконує блок. Дивіться це питання щодо відправки по головній черзі без блокування.
Зуль

3
Я дотримувався пропозиції @Zoul & nicktmro. Але виглядає, що це стане в тупиковий стан. Тестовий випадок '- [BlockTestTest testAsync]' розпочато. але так і не закінчилося
NSCry

3
Вам потрібно звільнити семафор під ARC?
Пітер Варбо

14
саме це я і шукав. Дякую! @PeterWarbo ні, ти цього не робиш. Використання ARC знімає необхідність робити dispatch_release ()
Hulvej

29

Окрім техніки семафору, вичерпно висвітленої в інших відповідях, тепер ми можемо використовувати XCTest у Xcode 6 для виконання асинхронних тестів через XCTestExpectation. Це виключає потребу в семафорах при тестуванні асинхронного коду. Наприклад:

- (void)testDataTask
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"asynchronous request"];

    NSURL *url = [NSURL URLWithString:@"http://www.apple.com"];
    NSURLSessionTask *task = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        XCTAssertNil(error, @"dataTaskWithURL error %@", error);

        if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
            NSInteger statusCode = [(NSHTTPURLResponse *) response statusCode];
            XCTAssertEqual(statusCode, 200, @"status code was not 200; was %d", statusCode);
        }

        XCTAssert(data, @"data nil");

        // do additional tests on the contents of the `data` object here, if you want

        // when all done, Fulfill the expectation

        [expectation fulfill];
    }];
    [task resume];

    [self waitForExpectationsWithTimeout:10.0 handler:nil];
}

Заради майбутніх читачів, хоча техніка диспетчерського семафору - це чудова техніка, коли це абсолютно потрібно, я мушу визнати, що я бачу занадто багато нових розробників, незнайомих з хорошими моделями асинхронного програмування, занадто швидко тяжіють до семафорів як загального механізму створення асинхронного підпрограми поводяться синхронно. Гірше, що я бачив, що багато хто з них використовує цей метод семафору з головної черги (і ми ніколи не повинні блокувати головну чергу у виробничих додатках).

Я знаю, що це не так (коли це питання було розміщено, не було приємного інструменту, як XCTestExpectation, наприклад, у цих наборах тестування ми повинні переконатися, що тест не закінчується, поки не буде виконано асинхронний виклик). Це одна з тих рідкісних ситуацій, коли може бути необхідна техніка семафору для блокування головної нитки.

Тож, вибачаючись перед автором цього оригінального питання, для якого звучить техніка семафору, я пишу це попередження всім тим новим розробникам, які бачать цю семафорну техніку і розглядають застосування її у своєму коді як загальний підхід для роботи з асинхронною методи: Попереджуйте, що дев'ять разів з десяти є семафорною технікою не такнайкращий підхід при підрахунку асинхронних операцій. Натомість ознайомтеся з блоками / шаблонами закриття, а також з моделями протоколів делегатів та сповіщеннями. Це часто набагато кращі способи вирішення асинхронних завдань, ніж використання семафорів, щоб змусити їх поводитися синхронно. Зазвичай є вагомі причини того, що асинхронні завдання покликані поводитись асинхронно, тому використовуйте правильний асинхронний зразок, а не намагайтеся змусити їх поводитись синхронно.


1
Я думаю, це зараз має бути прийнятою відповіддю. Ось також документи: developer.apple.com/library/prerelease/ios/documentation/…
hris.to

У мене питання з цього приводу. У мене є деякий асинхронний код, який виконує близько десятка дзвінків для завантаження AFNetworking для завантаження одного документа. Я хотів би запланувати завантаження на NSOperationQueue. Якщо я не використовую щось на зразок семафору, завантаження документів NSOperationвідразу ж з’явиться завершеним, і справжньої черги з завантаженнями не буде - вони майже продовжуватимуться одночасно, чого я не хочу. Чи розумні тут семафори? Або є кращий спосіб змусити NSOperations чекати асинхронного кінця інших? Або щось інше?
Бенджон

Ні, не використовуйте семафори в цій ситуації. Якщо у вас є черга операцій, до якої ви додаєте AFHTTPRequestOperationоб'єкти, то вам слід просто створити операцію завершення (яку ви зробите залежною від інших операцій). Або використовувати диспетчерські групи. До речі, ви кажете, що не хочете, щоб вони працювали одночасно, це добре, якщо це те, що вам потрібно, але ви сплачуєте серйозний штраф за виконання, роблячи це послідовно, а не одночасно. Я зазвичай використовую maxConcurrentOperationCount4 або 5.
Роб

28

Нещодавно я знову прийшов до цього питання і написав наступну категорію на тему NSObject:

@implementation NSObject (Testing)

- (void) performSelector: (SEL) selector
    withBlockingCallback: (dispatch_block_t) block
{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self performSelector:selector withObject:^{
        if (block) block();
        dispatch_semaphore_signal(semaphore);
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    dispatch_release(semaphore);
}

@end

Таким чином я можу легко перетворити асинхронний виклик із зворотним дзвінком у синхронний у тестах:

[testedObject performSelector:@selector(longAsyncOpWithCallback:)
    withBlockingCallback:^{
    STAssert
}];

24

Як правило, не використовуйте жоден із цих відповідей, вони часто не масштабуються (є винятки тут і там, звичайно)

Ці підходи несумісні з тим, як GCD призначений для роботи, і в кінцевому підсумку можуть спричинити тупикові наслідки та / або вбити акумулятор шляхом безперервного опитування.

Іншими словами, переставіть свій код так, щоб не було синхронного очікування результату, а замість цього розібрайтеся з тим, що результат буде повідомлений про зміну стану (наприклад, протоколи зворотних викликів / делегування, наявні, відмови, помилки тощо). (Вони можуть бути перероблені на блоки, якщо вам не подобається пекельний зворотний дзвінок.) Тому що це так, як викрити реальну поведінку для решти програми, ніж приховати її за фальшивим фасадом.

Замість цього використовуйте NSNotificationCenter , визначте спеціальний протокол делегування з зворотними зворотами для вашого класу. І якщо вам не подобається спілкуватися з делегатськими зворотними викликами з усієї сторони, загортайте їх у конкретний проксі-клас, який реалізує користувацький протокол і зберігає різні блоки у властивостях. Можливо, також надають зручності конструктори.

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

(Не вимагайте прикладу, тому що це банально, і нам довелося вкласти час, щоб також вивчити основи об’єктивних с.)


1
Це важливе попередження і через шаблони дизайну obj-C та
провіреність

8

Ось чудовий трюк, у якому не використовується семафор:

dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
    [object doSomething];
});
dispatch_sync(serialQ, ^{ });

Що ви робите, це чекати використання dispatch_syncпорожнього блоку для синхронного очікування на черзі послідовного відправлення до завершення блоку A-Synchronous.


Проблема з цією відповіддю полягає в тому, що вона не вирішує початкову проблему ОП, а саме те, що API, який потрібно використовувати, приймає в якості аргументу завершенняHandler і повертається негайно. Виклик цього API всередині блоку асинхронізації цього відповіді негайно повернеться, навіть не закінчившись завершенням. Тоді блок синхронізації буде виконуватися до завершенняHandler.
BTRUE

6
- (void)performAndWait:(void (^)(dispatch_semaphore_t semaphore))perform;
{
  NSParameterAssert(perform);
  dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
  perform(semaphore);
  dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  dispatch_release(semaphore);
}

Приклад використання:

[self performAndWait:^(dispatch_semaphore_t semaphore) {
  [self someLongOperationWithSuccess:^{
    dispatch_semaphore_signal(semaphore);
  }];
}];

2

Є також SenTestingKitAsync, який дозволяє писати такий код:

- (void)testAdditionAsync {
    [Calculator add:2 to:2 block^(int result) {
        STAssertEquals(result, 4, nil);
        STSuccess();
    }];
    STFailAfter(2.0, @"Timeout");
}

(Докладні відомості див. У статті objc.io. ) А оскільки Xcode 6 існує AsynchronousTestingкатегорія, XCTestяка дозволяє писати такий код:

XCTestExpectation *somethingHappened = [self expectationWithDescription:@"something happened"];
[testedObject doSomethigAsyncWithCompletion:^(BOOL succeeded, NSError *error) {
    [somethingHappened fulfill];
}];
[self waitForExpectationsWithTimeout:1 handler:NULL];

1

Ось альтернатива одного з моїх тестів:

__block BOOL success;
NSCondition *completed = NSCondition.new;
[completed lock];

STAssertNoThrow([self.client asyncSomethingWithCompletionHandler:^(id value) {
    success = value != nil;
    [completed lock];
    [completed signal];
    [completed unlock];
}], nil);    
[completed waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
[completed unlock];
STAssertTrue(success, nil);

1
У наведеному вище коді є помилка. З NSCondition документації до -waitUntilDate:"Ви повинні заблокувати приймач до виклику цього методу." Тож -unlockмає бути після -waitUntilDate:.
Патрік

Це не масштабується до нічого, що використовує кілька потоків або виконує черги.

0
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object blockToExecute:^{
    // ... your code to execute
    dispatch_semaphore_signal(sema);
}];

while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
    [[NSRunLoop currentRunLoop]
        runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0]];
}

Це зробило це для мене.


3
Що ж, це спричиняє велике використання процесора
кевін

4
@kevin Yup, це опитування гетто, яке вбиває акумулятор.

@Barry, як витрачається більше акумулятора. будь ласка керівництво.
pkc456

@ pkc456 Подивіться у книзі з інформатики про відмінності між тим, як працюють опитування та асинхронні повідомлення. Удачі.

2
Через чотири з половиною роки, і отримані знання та досвід я не рекомендував би відповісти.

0

Іноді також допомагають петлі тайм-ауту. Нехай ви зачекаєте, поки ви отримаєте якийсь сигнал (може бути BOOL) від методу зворотного виклику async, але що робити, якщо відповіді ніколи не буде, і ви хочете вийти з цього циклу? Тут нижче рішення, в основному відповіді вище, але з додаванням Timeout.

#define CONNECTION_TIMEOUT_SECONDS      10.0
#define CONNECTION_CHECK_INTERVAL       1

NSTimer * timer;
BOOL timeout;

CCSensorRead * sensorRead ;

- (void)testSensorReadConnection
{
    [self startTimeoutTimer];

    dispatch_semaphore_t sema = dispatch_semaphore_create(0);

    while (dispatch_semaphore_wait(sema, DISPATCH_TIME_NOW)) {

        /* Either you get some signal from async callback or timeout, whichever occurs first will break the loop */
        if (sensorRead.isConnected || timeout)
            dispatch_semaphore_signal(sema);

        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:[NSDate dateWithTimeIntervalSinceNow:CONNECTION_CHECK_INTERVAL]];

    };

    [self stopTimeoutTimer];

    if (timeout)
        NSLog(@"No Sensor device found in %f seconds", CONNECTION_TIMEOUT_SECONDS);

}

-(void) startTimeoutTimer {

    timeout = NO;

    [timer invalidate];
    timer = [NSTimer timerWithTimeInterval:CONNECTION_TIMEOUT_SECONDS target:self selector:@selector(connectionTimeout) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void) stopTimeoutTimer {
    [timer invalidate];
    timer = nil;
}

-(void) connectionTimeout {
    timeout = YES;

    [self stopTimeoutTimer];
}

1
Ця ж проблема: збій роботи акумулятора.

1
@Barry Не впевнений, навіть якщо ви подивилися на код. Існує TIMEOUT_SECONDS період, протягом якого, якщо виклик асинхронізації не відповість, він порушить цикл. Це хак, щоб зламати тупик. Цей код чудово працює, не вбиваючи акумулятор.
Хуля Сім Сім

0

Дуже примітивне рішення проблеми:

void (^nextOperationAfterLongOperationBlock)(void) = ^{

};

[object runSomeLongOperationAndDo:^{
    STAssert
    nextOperationAfterLongOperationBlock();
}];

0

Швидкий 4:

Використовуйте synchronousRemoteObjectProxyWithErrorHandlerзамістьremoteObjectProxy створення віддаленого об'єкта. Більше немає потреби в семафорі.

Нижче прикладу повернеться версія, отримана від проксі. Без цього synchronousRemoteObjectProxyWithErrorHandlerвона вийде з ладу (намагаючись отримати доступ до недоступної пам'яті):

func getVersion(xpc: NSXPCConnection) -> String
{
    var version = ""
    if let helper = xpc.synchronousRemoteObjectProxyWithErrorHandler({ error in NSLog(error.localizedDescription) }) as? HelperProtocol
    {
        helper.getVersion(reply: {
            installedVersion in
            print("Helper: Installed Version => \(installedVersion)")
            version = installedVersion
        })
    }
    return version
}

-1

Мені доводиться чекати, поки завантажується UIWebView, перш ніж запустити мій метод, я зміг домогтися цього, виконавши UIWebView готові перевірки основної нитки за допомогою GCD в поєднанні з методами семафору, згаданими в цій темі. Кінцевий код виглядає приблизно так:

-(void)myMethod {

    if (![self isWebViewLoaded]) {

            dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

            __block BOOL isWebViewLoaded = NO;

            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

                while (!isWebViewLoaded) {

                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((0.0) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        isWebViewLoaded = [self isWebViewLoaded];
                    });

                    [NSThread sleepForTimeInterval:0.1];//check again if it's loaded every 0.1s

                }

                dispatch_sync(dispatch_get_main_queue(), ^{
                    dispatch_semaphore_signal(semaphore);
                });

            });

            while (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW)) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0]];
            }

        }

    }

    //Run rest of method here after web view is loaded

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.