あなたは(コメントで)「非同期メソッドは、明示的なスレッドを使用せずに簡単な非同期性を提供します」と述べました。しかし、あなたの不満は、非同期メソッドで何かをしようとしているようで、簡単ではありません。ここに矛盾があると思いますか?
コールバック ベースの設計を使用すると、言語の組み込み構造を使用して制御フローを直接表現する機能が犠牲になります。
したがって、コールバック ベースの設計の使用をやめることをお勧めします。グランド セントラル ディスパッチ (GCD) を使用すると、"バックグラウンドで" 作業を実行し、メイン スレッドにコールバックしてユーザー インターフェイスを更新することが簡単になります (この言葉もまた!)。したがって、API の同期バージョンがある場合は、それをバックグラウンド キューで使用するだけです。
- (void)interactWithRemoteAPI:(id<RemoteAPI>)remoteAPI {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// This block runs on a background queue, so it doesn't block the main thread.
// But it can't touch the user interface.
for (NSURL *url in @[url1, url2, url3, url4]) {
int status = [remoteAPI syncRequestWithURL:url];
if (status != 0) {
dispatch_async(dispatch_get_main_queue(), ^{
// This block runs on the main thread, so it can update the
// user interface.
[self remoteRequestFailedWithURL:url status:status];
});
return;
}
}
});
}
通常の制御フローを使用しているだけなので、より複雑なことを行うのは簡単です。2 つのリクエストを発行し、最大 100k のチャンクでファイルをアップロードしてから、もう 1 つのリクエストを発行する必要があるとします。
#define AsyncToMain(Block) dispatch_async(dispatch_get_main_queue(), Block)
- (void)uploadFile:(NSFileHandle *)fileHandle withRemoteAPI:(id<RemoteAPI>)remoteAPI {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int status = [remoteAPI syncRequestWithURL:url1];
if (status != 0) {
AsyncToMain(^{ [self remoteRequestFailedWithURL:url1 status:status]; });
return;
}
status = [remoteAPI syncRequestWithURL:url2];
if (status != 0) {
AsyncToMain(^{ [self remoteRequestFailedWithURL:url2 status:status]; });
return;
}
while (1) {
// Manage an autorelease pool to avoid accumulating all of the
// 100k chunks in memory simultaneously.
@autoreleasepool {
NSData *chunk = [fileHandle readDataOfLength:100 * 1024];
if (chunk.length == 0)
break;
status = [remoteAPI syncUploadChunk:chunk];
if (status != 0) {
AsyncToMain(^{ [self sendChunkFailedWithStatus:status]; });
return;
}
}
}
status = [remoteAPI syncRequestWithURL:url4];
if (status != 0) {
AsyncToMain(^{ [self remoteRequestFailedWithURL:url4 status:status]; });
return;
}
AsyncToMain(^{ [self uploadFileSucceeded]; });
});
}
きっとあなたは「ああ、それは素晴らしいですね」と言っているに違いありません。RemoteAPI
;^) しかし、「同期メソッドではなく、非同期メソッドしかない
場合はどうなるでしょうか?」と言うかもしれません。</p>
GCD を使用して、非同期メソッドの同期ラッパーを作成できます。ラッパーが async メソッドを呼び出すようにし、async メソッドがコールバックを呼び出すまでブロックする必要があります。ややこしいのは、非同期メソッドがコールバックを呼び出すためにどのキューを使用するか、またそれがコールバックを呼び出すために使用するかどうかがわからないことですdispatch_sync
。したがって、並行キューから async メソッドを呼び出すことで安全を確保しましょう。
- (int)syncRequestWithRemoteAPI:(id<RemoteAPI>)remoteAPI url:(NSURL *)url {
__block int outerStatus;
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[remoteAPI asyncRequestWithURL:url completion:^(int status) {
outerStatus = status;
dispatch_semaphore_signal(sem);
}];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_release(sem);
return outerStatus;
}
アップデート
最初に 3 番目のコメントに返信し、次に 2 番目のコメントに返信します。
3 番目のコメント
3 番目のコメント:
最後になりましたが、別のスレッドを専用の呼び出しの同期バージョンをラップするソリューションは、非同期の代替手段を使用するよりもコストがかかります。スレッドは高価なリソースであり、ブロックしているときは基本的に 1 つのスレッドを失っています。非同期呼び出し (少なくとも OS ライブラリ内のもの) は、通常、はるかに効率的な方法で処理されます。(たとえば、同時に 10 個の URL を要求した場合、10 個のスレッドを起動しない可能性があります (またはそれらをスレッドプールに入れません))。
はい、スレッドを使用すると、非同期呼び出しを使用するよりもコストがかかります。だから何?問題は、それが高すぎるかどうかです。Objective-C メッセージは、現在の iOS ハードウェアの一部のシナリオ (たとえば、リアルタイムの顔検出アルゴリズムや音声認識アルゴリズムの内部ループ) ではコストがかかりすぎますが、ほとんどの場合、それらを使用することに何の不安もありません。
スレッドが「高価なリソース」であるかどうかは、コンテキストに依存します。あなたの例を考えてみましょう:「たとえば、同時に 10 個の URL を要求した場合、10 個のスレッドを起動しない可能性があります (またはそれらをスレッドプールに入れません)」。確認してみましょう。
NSURL *url = [NSURL URLWithString:@"http://1.1.1.1/"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
for (int i = 0; i < 10; ++i) {
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
NSLog(@"response=%@ error=%@", response, error);
}];
}
ここでは、Apple 独自の推奨+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]
方法を使用して、10 個のリクエストを非同期に送信しています。私は URL を非応答にするように選択したので、Apple がこのメソッドを実装するためにどのような種類のスレッド/キュー戦略を使用しているかを正確に知ることができます。iOS 6.0.1 を実行している iPhone 4S でアプリを実行し、デバッガーで一時停止して、スレッド ナビゲーターのスクリーン ショットを撮りました。
というラベルのスレッドが 10 個あることがわかりますcom.apple.root.default-priority
。そのうちの 3 つを開いたので、それらが通常の GCD キュー スレッドであることがわかります。それぞれが で定義されたブロック+[NSURLConnection sendAsynchronousRequest:…]
を呼び出します+[NSURLConnection sendSynchronousRequest:…]
。10 個すべてを確認しましたが、スタック トレースはすべて同じです。したがって、実際には、OS ライブラリは 10 個のスレッドをスピンアップします。
ループ数を 10 から 100 に増やしたところ、GCD がcom.apple.root.default-priority
スレッド数を 64 に制限していることがわかりました。したがって、私が発行した他の 36 の要求は、グローバルなデフォルト優先度キューに入れられ、実行が開始されるまでは開始されません。 64 の「実行中」のリクエストの一部が終了します。
では、スレッドを使用して非同期関数を同期関数に変えるのはコストが高すぎるのでしょうか? これらを同時にいくつ行うかにもよると思います。数が 10 を下回っても、20 を下回っても、何の心配もありません。
2 番目のコメント
これにより、2番目のコメントが表示されます。
ただし、これらの 3 つのことを同時に実行し、それらの「いずれか」が終了したら、残りを無視してこれら 3 つの呼び出しを同時に実行し、「すべて」が終了すると成功します。
これらは GCD を使用するのが簡単なケースですが、必要に応じて GCD と非同期のアプローチを組み合わせて、使用するスレッドを減らしながら、制御フローに言語ネイティブ ツールを使用することもできます。
最初に、後で入力する手間を省くために、リモート API 補完ブロックの typedef を作成します。
typedef void (^RemoteAPICompletionBlock)(int status);
制御フローを前と同じ方法で、メイン スレッドから同時実行キューに移動して開始します。
- (void)complexFlowWithRemoteAPI:(id<RemoteAPI>)remoteAPI {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
まず、3 つのリクエストを同時に発行し、そのうちの 1 つが成功するまで (または、おそらく 3 つすべてが失敗するまで) 待機します。
statusOfFirstRequestToSucceed
では、任意の数の非同期リモート API 要求を発行し、最初の要求が成功するまで待機する関数 があるとします。この関数は、各非同期リクエストの完了ブロックを提供します。しかし、異なるリクエストは異なる引数を受け取る可能性があります... API リクエストを関数に渡すにはどうすればよいでしょうか?
APIリクエストごとにリテラルブロックを渡すことでそれを行うことができます。各リテラル ブロックは完了ブロックを受け取り、非同期リモート API 要求を発行します。
int status = statusOfFirstRequestToSucceed(@[
^(RemoteAPICompletionBlock completion) {
[remoteAPI requestWithCompletion:completion];
},
^(RemoteAPICompletionBlock completion) {
[remoteAPI anotherRequestWithCompletion:completion];
},
^(RemoteAPICompletionBlock completion) {
[remoteAPI thirdRequestWithCompletion:completion];
}
]);
if (status != 0) {
AsyncToMain(^{ [self complexFlowFailedOnFirstRoundWithStatus:status]; });
return;
}
OK、これで最初の 3 つの並列リクエストを発行し、1 つが成功するか、すべてが失敗するのを待ちました。次に、さらに 3 つの並列リクエストを発行し、すべてが成功するか、そのうちの 1 つが失敗するのを待ちます。したがって、関数を想定することを除いて、ほぼ同じですstatusOfFirstRequestToFail
。
status = statusOfFirstRequestToFail(@[
^(RemoteAPICompletionBlock completion) {
[remoteAPI requestWithCompletion:completion];
},
^(RemoteAPICompletionBlock completion) {
[remoteAPI anotherRequestWithCompletion:completion];
},
^(RemoteAPICompletionBlock completion) {
[remoteAPI thirdRequestWithCompletion:completion];
}
]);
if (status != 0) {
AsyncToMain(^{ [self complexFlowFailedOnSecondRoundWithStatus:status]; });
return;
}
並列リクエストの両方のラウンドが終了したので、メイン スレッドに成功を通知できます。
[self complexFlowSucceeded];
});
}
全体として、これは非常に単純な制御フローのように思えます。実装する必要があるのは and だけstatusOfFirstRequestToSucceed
ですstatusOfFirstRequestToFail
。余分なスレッドなしでそれらを実装できます。それらは非常に似ているため、実際の作業を行うヘルパー関数を両方とも呼び出すようにします。
static int statusOfFirstRequestToSucceed(NSArray *requestBlocks) {
return statusOfFirstRequestWithStatusPassingTest(requestBlocks, ^BOOL (int status) {
return status == 0;
});
}
static int statusOfFirstRequestToFail(NSArray *requestBlocks) {
return statusOfFirstRequestWithStatusPassingTest(requestBlocks, ^BOOL (int status) {
return status != 0;
});
}
ヘルパー関数では、競合状態を防ぐために、完了ブロックを実行するためのキューが必要です。
static int statusOfFirstRequestWithStatusPassingTest(NSArray *requestBlocks,
BOOL (^statusTest)(int status))
{
dispatch_queue_t completionQueue = dispatch_queue_create("remote API completion", 0);
completionQueue
usingdispatch_sync
にのみブロックを配置しdispatch_sync
、キューがメイン キューでない限り、常に現在のスレッドでブロックを実行することに注意してください。
また、一部のリクエストが成功ステータスで完了したとき、またはすべてのリクエストが完了したときに外部関数を起動するために、セマフォも必要です。
dispatch_semaphore_t enoughJobsCompleteSemaphore = dispatch_semaphore_create(0);
まだ完了していないジョブの数と、最後に完了したジョブのステータスを追跡します。
__block int jobsLeft = requestBlocks.count;
__block int outerStatus = 0;
jobsLeft
が 0 になると、テストをパスするステータスに設定したか、すべてのジョブが完了したことを意味しますouterStatus
。これは、待機が完了したかどうかを追跡する作業を行う完了ブロックです。リモート API が複数の完了ブロックを並行して (個別のスレッドまたは同時キューで) ディスパッチする場合に備えて、andへのcompletionQueue
アクセスをシリアル化するためにすべてを実行します。jobsLeft
outerStatus
RemoteAPICompletionBlock completionBlock = ^(int status) {
dispatch_sync(completionQueue, ^{
外部関数がまだ現在のジョブの完了を待っているかどうかを確認します。
if (jobsLeft == 0) {
// The outer function has already returned.
return;
}
次に、残りのジョブ数を減らし、完了したジョブのステータスを外部関数で利用できるようにします。
--jobsLeft;
outerStatus = status;
完了したジョブのステータスがテストに合格した場合、jobsLeft
他のジョブが私のステータスを上書きしたり、外側の関数を単一化したりしないようにゼロに設定します。
if (statusTest(status)) {
// We have a winner. Prevent other jobs from overwriting my status.
jobsLeft = 0;
}
待機するジョブが残っていない場合 (ジョブがすべて終了したか、このジョブのステータスがテストに合格したため)、外側の関数を起こします。
if (jobsLeft == 0) {
dispatch_semaphore_signal(enoughJobsCompleteSemaphore);
}
最後に、キューとセマフォを解放します。(保持は後で、リクエスト ブロックをループして実行するときに行われます。)
dispatch_release(completionQueue);
dispatch_release(enoughJobsCompleteSemaphore);
});
};
これで完了ブロックは終了です。関数の残りの部分は簡単です。最初に各リクエスト ブロックを実行し、ダングリング リファレンスを防ぐためにキューとセマフォを保持します。
for (void (^requestBlock)(RemoteAPICompletionBlock) in requestBlocks) {
dispatch_retain(completionQueue); // balanced in completionBlock
dispatch_retain(enoughJobsCompleteSemaphore); // balanced in completionBlock
requestBlock(completionBlock);
}
ARC を使用していて、展開ターゲットが iOS 6.0 以降の場合、retains は必要ないことに注意してください。
次に、ジョブの 1 つが私を起こし、キューとセマフォを解放し、私を目覚めさせたジョブのステータスを返すのを待ちます。
dispatch_semaphore_wait(enoughJobsCompleteSemaphore, DISPATCH_TIME_FOREVER);
dispatch_release(completionQueue);
dispatch_release(enoughJobsCompleteSemaphore);
return outerStatus;
}
の構造はかなり一般的であることに注意してください。それぞれが完了ブロックを呼び出してステータスstatusOfFirstRequestWithStatusPassingTest
を渡す限り、任意の要求ブロックを渡すことができます。int
関数を変更して、各リクエスト ブロックからのより複雑な結果を処理したり、未処理のリクエストをキャンセルしたりできます (キャンセル API がある場合)。