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回、セマフォの手法はnotであり、非同期操作をカウントする場合の最良のアプローチであることに注意してください。代わりに、完了ブロック/閉鎖パターン、およびデリゲートプロトコルパターンと通知に精通してください。これらは多くの場合、セマフォを使用して同期タスクを実行するよりも、非同期タスクを処理するはるかに優れた方法です。通常、非同期タスクが非同期に動作するように設計されているのには十分な理由があるため、同期タスクを実行させるのではなく、適切な非同期パターンを使用してください。
私は最近この問題に再び来て、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_sync
を使用して待機し、A-Synchronousブロックが完了するまでシリアルディスパッチキューで同期的に待機します。
- (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];
ここに私のテストの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);
スウィフト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
}
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]];
}
これは私のためにそれをしました。
タイムアウトループも役立つ場合があります。非同期コールバックメソッドから(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();
}];