非同期にディスパッチされたブロックが完了するのを待つにはどうすればよいですか?


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でセマフォを解放する必要がありますか?
Peter Warbo 2013

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。また、これらのテストスイートでは、非同期呼び出しが完了するまでテストが終了しないことを確認する必要があります)。これは、メインスレッドをブロックするためのセマフォテクニックが必要になるまれな状況の1つです。

したがって、セマフォ手法が適切であるこの元の質問の作成者に謝罪し、このセマフォ手法を理解しているすべての新しい開発者にこの警告を書き込み、非同期を処理するための一般的なアプローチとしてコードに適用することを検討します方法は:10のうち9回は、セマフォ技術があることをあらかじめご了承ません非同期操作に遭遇するときの最良のアプローチ。代わりに、完了ブロック/クロージャーパターン、およびデリゲートプロトコルパターンと通知をよく理解してください。これらは多くの場合、セマフォを使用して同期的に動作させるよりも、非同期タスクを処理するはるかに優れた方法です。通常、非同期タスクが非同期的に動作するように設計されていることには十分な理由があるため、同期的に動作させるのではなく、適切な非同期パターンを使用してください。


1
これは今や受け入れられるべき答えだと思います。ここではドキュメントもある:developer.apple.com/library/prerelease/ios/documentation/...
hris.to

これについて質問があります。単一のドキュメントをダウンロードするために約12の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を使用して、クラスのコールバックでカスタムデリゲートプロトコルを定義します。デリゲートコールバックをいじくり回したくない場合は、カスタムプロトコルを実装し、さまざまなブロックをプロパティに保存する具体的なプロキシクラスにラップします。おそらく便利なコンストラクタも提供します。

最初の作業は少し多めですが、長期的に見れば、ひどい競合状態とバッテリー殺害のポーリングの数が減ります。

(例は取るに足らないものであり、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_sync空のブロックを使用して、A-Synchronousブロックが完了するまで、シリアルディスパッチキューで同期的に待機することを行います。


この回答の問題は、OPの元の問題に対応していないことです。これは、使用する必要のあるAPIが引数としてcompletionHandlerを取り、すぐに戻ることです。この回答の非同期ブロック内でそのAPIを呼び出すと、completionHandlerがまだ実行されていなくても、すぐに戻ります。その後、完了ブロックの前に同期ブロックが実行されます。
BTRUE 2017

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

ここに私のテストの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うん、これはバッテリーを殺すゲットー投票です。

@バリー、それはどのように多くのバッテリーを消費しますか?ご案内ください。
pkc456 2017

@ 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 14

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.