非同期に発送されたブロックが終了するのを待つにはどうすればよいですか?
-
29-09-2019 - |
質問
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_sempahore
. 。このように見えるはずです:
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[object runSomeLongOperationAndDo:^{
STAssert…
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
dispatch_release(sema);
これは、もしあっても正しく動作するはずです 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
;また、これらのテストスイートでは、非同期呼び出しが完了するまでテストが終了しないようにする必要があります)。これは、メインスレッドをブロックするためのセマフォテクニックが必要になる可能性のあるまれな状況の1つです。
そのため、セマフォのテクニックが健全であるこの元の質問の著者に謝罪することで、このセマフォのテクニックを見て、コードに適用することを非同期に対処するための一般的なアプローチとしてコードに適用することを検討するすべての新しい開発者にこの警告を書きます。方法:10のうち9回、セマフォのテクニックは いいえ 非同期操作に遭遇する際の最良のアプローチ。代わりに、完了ブロック/閉鎖パターン、およびDelegate-Protocolパターンと通知に慣れてください。これらは、セマフォを使用して同期的に振る舞うのではなく、非同期タスクに対処するためのはるかに優れた方法です。通常、非同期タスクが非同期に振る舞うように設計された理由があるため、それらを同期して振る舞うのではなく、適切な非同期パターンを使用します。
私は最近この問題に再び来て、次のカテゴリを書きました 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がどのように機能するかと互換性がなく、最終的にはデッドロックを引き起こしたり、ノンストップ投票でバッテリーを殺したりすることになります。
言い換えれば、結果を待機する同期がないようにコードを再配置しますが、代わりに状態の変更(コールバック/デリゲートプロトコル、利用可能、去る、エラーなど)を通知される結果に対処します。 (コールバックHellが気に入らない場合は、これらをブロックにリファクタリングできます。)これは、偽のファサードの後ろに隠すよりも、アプリの残りの部分に実際の動作を公開する方法だからです。
代わりに、使用します nsnotificationCenter, 、クラスのコールバックを使用して、カスタムデリゲートプロトコルを定義します。また、デリゲートコールバックと一緒にマッキングするのが好きではない場合は、カスタムプロトコルを実装し、プロパティのさまざまなブロックを保存する具体的なプロキシクラスに包みます。おそらく便利なコンストラクターも提供します。
最初の作業はわずかに多いですが、長期的にはひどいレース条件とバッテリーを殺す投票の数を減らします。
(例を尋ねないでください。それは些細なことであり、Objective-Cの基本を学ぶために時間を費やさなければならなかったからです。)
これがセマフォを使用しない気の利いたトリックです:
dispatch_queue_t serialQ = dispatch_queue_create("serialQ", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQ, ^
{
[object doSomething];
});
dispatch_sync(serialQ, ^{ });
あなたがしていることは、使用するのを待つことです dispatch_sync
空のブロックを使用すると、同期ブロックが完了するまで、シリアルディスパッチキューを同期して待ちます。
- (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の記事 詳細については。)そしてXcode6なので 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);
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]];
}
これは私のためにそれをしました。
時には、タイムアウトループも役立ちます。 Async Callbackメソッドから(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];
}
問題に対する非常に原始的な解決策:
void (^nextOperationAfterLongOperationBlock)(void) = ^{
};
[object runSomeLongOperationAndDo:^{
STAssert…
nextOperationAfterLongOperationBlock();
}];
Swift4:
使用する 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
}