[objective-c] 비동기 적으로 디스패치 된 블록이 완료 될 때까지 어떻게 대기합니까?

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있습니다.



답변

을 사용하려고합니다 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:작업이 실제로 스레드를 수행 할만큼 충분히 길지 않고 대신 동 기적으로 실행된다고 결정 하더라도 올바르게 작동해야합니다 .


답변

다른 답변에서 철저하게 다루는 세마포어 기술 외에도 이제 Xcode 6의 XCTest를 사용하여를 통해 비동기 테스트를 수행 할 수 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. 또한 이러한 테스트 스위트에서는 비동기 호출이 완료 될 때까지 테스트가 완료되지 않아야합니다). 이것은 메인 스레드를 차단하기위한 세마포어 기술이 필요할 수있는 드문 상황 중 하나입니다.

따라서 세마포어 기술이 적합한이 원래 질문의 저자에게 사과를 드리며이 세마포어 기술을보고 비동기식 처리를위한 일반적인 접근 방식으로 코드에 적용하는 것을 고려하는 모든 새로운 개발자 에게이 경고를 씁니다. 방법 : 10 명 중 아홉 번, 세마포어 기술은 미리 양해 없습니다비동기 작업을 처리 할 때 가장 좋은 방법입니다. 대신 완료 프로토콜 / 클로저 패턴과 델리게이트 프로토콜 패턴 및 알림을 숙지하십시오. 이들은 종종 세마포어를 사용하여 동 기적으로 동작하는 것보다 비동기 작업을 처리하는 훨씬 좋은 방법입니다. 일반적으로 비동기 타스크가 비동기 적으로 작동하도록 설계된 이유는 충분하므로 동 기적으로 작동하지 말고 올바른 비동기 패턴을 사용하십시오.


답변

최근 에이 문제에 다시 와서 다음 카테고리를 작성했습니다 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
}];


답변

일반적으로 이러한 답변을 사용하지 않으면 종종 확장되지 않습니다 (예 : 여기와 거기에 예외가 있습니다)

이러한 접근 방식은 GCD의 작동 방식과 호환되지 않으며 중단없는 폴링으로 교착 상태를 유발하거나 배터리를 종료시킵니다.

즉, 결과를 기다리는 동기가 없도록 코드를 다시 정렬하는 대신 상태 변경 (예 : 콜백 / 델리게이트 프로토콜, 사용 가능, 종료, 오류 등)에 대한 알림을받는 결과를 처리하십시오. 콜백 지옥이 마음에 들지 않으면 이들은 블록으로 리팩토링 될 수 있습니다. 이것은 거짓된 파사드 뒤에 숨기는 것보다 앱의 나머지 부분에 실제 행동을 노출시키는 방법입니다.

대신 NSNotificationCenter 를 사용하고 클래스에 대한 콜백을 사용하여 사용자 정의 델리게이트 프로토콜을 정의하십시오. 또한 델리게이트 콜백을 사용하지 않고 커스텀 프로토콜을 구현하고 다양한 블록을 속성에 저장하는 구체적인 프록시 클래스로 래핑하는 것을 좋아하지 않습니다. 아마도 편리한 생성자도 제공 할 것입니다.

초기 작업은 약간 더 많지만 장기적으로 끔찍한 경쟁 조건 및 배터리 살해 폴링 수를 줄입니다.

(사소한 것이기 때문에 objective-c 기초를 배우기 위해 시간을 투자해야했기 때문에 예를 요구하지 마십시오.)


답변

세마포어를 사용하지 않는 멋진 트릭이 있습니다.

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

dispatch_syncA- 동기 블록이 완료 될 때까지 직렬 디스패치 큐를 동 기적으로 대기하기 위해 빈 블록을 사용하여 대기하십시오.


답변

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


답변

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