您说(在评论中),“异步方法无需使用显式线程即可提供简单的异步性。” 但是您的抱怨似乎是您正在尝试使用异步方法做某事,这并不容易。你看到这里的矛盾了吗?
当您使用基于回调的设计时,您牺牲了直接使用语言的内置结构表达控制流的能力。
所以我建议你停止使用基于回调的设计。Grand Central Dispatch (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;
}
}
});
}
因为我们只是使用正常的控制流,所以做更复杂的事情很简单。假设我们需要发出两个请求,然后以最多 100k 的块上传一个文件,然后再发出一个请求:
#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 为异步方法创建同步包装器。我们需要让包装器调用异步方法,然后阻塞直到异步方法调用回调。棘手的一点是,我们可能不知道异步方法使用哪个队列来调用回调,也不知道它是否用于dispatch_sync
调用回调。因此,让我们通过从并发队列中调用异步方法来确保安全。
- (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;
}
更新
我会先回复您的第三条评论,然后再回复您的第二条评论。
第三条评论
您的第三条评论:
最后但并非最不重要的一点是,您使用一个单独的线程来环绕调用的同步版本的解决方案比使用异步替代方案的成本更高。线程是一种昂贵的资源,当它阻塞时,您基本上已经丢失了一个线程。异步调用(至少在 OS 库中的调用)通常以更有效的方式处理。(例如,如果您同时请求 10 个 url,很可能它不会启动 10 个线程(或将它们放入线程池中))
是的,使用线程比仅使用异步调用更昂贵。所以呢?问题是它是否太贵了。在当前 iOS 硬件上的某些场景中,Objective-C 消息过于昂贵(例如,实时人脸检测或语音识别算法的内部循环),但我大部分时间都不会担心使用它们。
线程是否是“昂贵的资源”实际上取决于上下文。让我们考虑一下您的示例:“例如,如果您同时请求 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);
}];
}
所以这里我使用苹果自己推荐的+[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]
方法异步发送10个请求。我选择了不响应的 URL,因此我可以准确地看到 Apple 使用哪种线程/队列策略来实现此方法。我在运行 iOS 6.0.1 的 iPhone 4S 上运行该应用程序,在调试器中暂停,并截取了 Thread Navigator 的屏幕截图:
![10 NSURLConnection sendAsynchronousRequest:线程](https://i.stack.imgur.com/8DkL2.png)
您可以看到有 10 个线程标记为com.apple.root.default-priority
。我已经打开了其中的三个,因此您可以看到它们只是普通的 GCD 队列线程。每个调用一个定义在 中的块+[NSURLConnection sendAsynchronousRequest:…]
,它只是转身并调用+[NSURLConnection sendSynchronousRequest:…]
. 我检查了所有 10 个,它们都有相同的堆栈跟踪。所以,事实上,操作系统库确实启动了 10 个线程。
我将循环计数从 10 增加到 100,发现 GCD 将com.apple.root.default-priority
线程数限制为 64。所以我的猜测是我发出的其他 36 个请求在全局默认优先级队列中排队,甚至不会开始执行,直到64 个“正在运行”的请求中的一些完成了。
那么,使用线程将异步函数转为同步函数是否成本太高?我会说这取决于你计划同时做多少。如果这个数字低于 10 甚至 20,我会毫不犹豫。
第二条评论
这让我想到了你的第二条评论:
然而,当你有:同时做这三件事,当它们中的“任何”完成时,然后忽略其余的,同时做这三个调用,当它们全部完成时,就会成功。
在这些情况下,使用 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), ^{
首先,我们要同时发出三个请求,并等待其中一个成功(或者,大概三个都失败)。
因此,假设我们有一个函数 ,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;
}
好的,现在我们已经发出了前三个并行请求,并等待一个成功,或者所有它们都失败。现在我们要再发出三个并行请求并等待所有请求都成功,或者其中一个请求失败。所以它几乎是相同的,除了我要假设一个函数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];
});
}
总的来说,这对我来说似乎是一个非常简单的控制流程,我们只需要实现statusOfFirstRequestToSucceed
and 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
using上dispatch_sync
,并且dispatch_sync
始终在当前线程上运行块,除非队列是主队列。
我还需要一个信号量,以在某些请求以传递状态完成或所有请求都完成时唤醒外部函数:
dispatch_semaphore_t enoughJobsCompleteSemaphore = dispatch_semaphore_create(0);
我将跟踪尚未完成的作业数量和最后一个要完成的作业的状态:
__block int jobsLeft = requestBlocks.count;
__block int outerStatus = 0;
当jobsLeft
变为 0 时,表示我已设置outerStatus
为通过测试的状态,或者所有作业都已完成。这是完成块,我将在其中跟踪我是否完成等待。如果远程 API 并行调度多个完成块(在单独的线程或并发队列上),我将全部执行completionQueue
以序列化对jobsLeft
and的访问: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 或更高版本,则不需要保留。
然后我只是等待其中一个作业唤醒我,释放队列和信号量,并返回唤醒我的作业的状态:
dispatch_semaphore_wait(enoughJobsCompleteSemaphore, DISPATCH_TIME_FOREVER);
dispatch_release(completionQueue);
dispatch_release(enoughJobsCompleteSemaphore);
return outerStatus;
}
请注意,结构statusOfFirstRequestWithStatusPassingTest
是相当通用的:您可以传递任何您想要的请求块,只要每个请求块调用完成块并传入一个int
状态。您可以修改该函数以处理来自每个请求块的更复杂的结果,或取消未完成的请求(如果您有取消 API)。