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


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

을 사용하려고합니다 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
아마도 완료 블록이 기본 대기열로 발송 되었기 때문일까요? 큐는 세마포어를 기다리는 동안 차단되므로 절대로 블록을 실행하지 않습니다. 차단하지 않고 메인 큐에서 디스패치에 대한 이 질문을 참조하십시오 .
zoul

3
@Zoul & nicktmro의 제안을 따랐습니다. 그러나 교착 상태가 될 것으로 보입니다. 테스트 케이스 '-[BlockTestTest testAsync]'가 시작되었습니다. 그러나 끝나지 않았다
NSCry

3
ARC에서 세마포어를 해제해야합니까?
피터 워보

14
이것은 내가 찾던 것입니다. 감사! @PeterWarbo 아니 당신은하지 않습니다. ARC를 사용하면 dispatch_release ()를 수행 할 필요가 없습니다.
Hulvej

29

다른 답변에서 철저하게 다루는 세마포어 기술 외에도 이제 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 명 중 아홉 번, 세마포어 기술은 미리 양해 없습니다비동기 작업을 처리 할 때 가장 좋은 방법입니다. 대신 완료 프로토콜 / 클로저 패턴과 델리게이트 프로토콜 패턴 및 알림을 숙지하십시오. 이들은 종종 세마포어를 사용하여 동 기적으로 동작하는 것보다 비동기 작업을 처리하는 훨씬 좋은 방법입니다. 일반적으로 비동기 타스크가 비동기 적으로 작동하도록 설계된 이유는 충분하므로 동 기적으로 작동하지 말고 올바른 비동기 패턴을 사용하십시오.


1
나는 이것이 지금 받아 들여지는 대답이라고 생각합니다. 다음 문서도 있습니다 : developer.apple.com/library/prerelease/ios/documentation/…
hris.to

이것에 대한 질문이 있습니다. 단일 문서를 다운로드하기 위해 수십 개의 AFNetworking 다운로드 호출을 수행하는 비동기 코드가 있습니다. 에 다운로드를 예약하고 싶습니다 NSOperationQueue. 세마포어와 같은 것을 사용하지 않는 한 문서 다운로드 NSOperation는 모두 즉시 완료된 것으로 보이며 다운로드가 실제로 대기하지는 않습니다. 거의 동시에 진행될 것이므로 원하지 않습니다. 여기서 세마포어가 합리적입니까? 아니면 NSOperations가 다른 사람의 비동기 끝을 기다리는 더 좋은 방법이 있습니까? 또는 다른 것?
Benjohn

아니요,이 상황에서는 세마포어를 사용하지 마십시오. AFHTTPRequestOperation객체를 추가 할 작업 대기열이있는 경우 완료 작업 (다른 작업에 따라 달라짐)을 만들어야합니다. 또는 디스패치 그룹을 사용하십시오. BTW, 당신은 그것들이 동시에 실행되는 것을 원하지 않는다고 말하는데, 이것이 필요한 것이라면 괜찮지 만, 동시에이 아닌 순차적 으로이 작업을 수행하면 심각한 성능 저하가 발생합니다. 나는 일반적으로 maxConcurrentOperationCount4 또는 5를 사용합니다 .
Rob

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

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

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


1
obj-C 디자인 패턴과 테스트 가능성 때문에 중요한 경고입니다
BootMaker

8

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

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

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


이 답변의 문제는 OP의 원래 문제를 해결하지 못한다는 것입니다. 즉, 사용해야하는 API는 인수로 completionHandler를 인수로 가져 와서 즉시 반환합니다. 이 응답의 비동기 블록 내에서 해당 API를 호출하면 completionHandler가 아직 실행되지 않았더라도 즉시 반환됩니다. 그런 다음 completeHandler 전에 sync 블록이 실행됩니다.
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:.
Patrick

이것은 여러 스레드를 사용하거나 대기열을 실행하는 것으로 확장되지 않습니다.

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
글쎄, 그것은 비록 높은 CPU 사용량을 야기
케빈

4
@ kevin Yup, 이것은 배터리를 죽일 빈민가 폴링입니다.

@ 배리, 배터리를 더 많이 소비하는 방법. 안내 해주세요.
pkc456

@ pkc456 컴퓨터 과학 책에서 폴링과 비동기 알림의 작동 방식의 차이점에 대해 살펴보십시오. 행운을 빕니다.

2
4 년 반이 지난 지금, 얻은 지식과 경험을 바탕으로 답변을 추천하지 않습니다.

0

때때로 타임 아웃 루프도 도움이됩니다. 비동기 콜백 메소드에서 일부 (BOOL 일 수 있음) 신호를 얻을 때까지 기다릴 수 있지만 응답이 없으면 루프에서 벗어나고 싶습니까? 아래는 대부분 위에서 대답했지만 시간 초과가 추가 된 솔루션입니다.

#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 기간이 있습니다. 그것은 교착 상태를 해킹하는 해킹입니다. 이 코드는 배터리를 죽이지 않고 완벽하게 작동합니다.
Khulja Sim Sim

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 가로 드 될 때까지 기다려야합니다.이 스레드에서 언급 한 세마포어 메소드와 함께 GCD를 사용하여 메인 스레드에서 UIWebView 준비 검사를 수행 하여이 작업을 수행 할 수있었습니다. 최종 코드는 다음과 같습니다.

-(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

}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.