При использовании методов, которые возвращают блоки, они могут быть очень удобными.
Тем не менее, когда вам нужно объединить несколько из них, это быстро становится беспорядочным.
Итак, для каждой итерации я иду на один уровень глубже, и я даже не обрабатываю ошибки во вложенных блоках.
Ухудшается, когда есть реальный цикл. Например, скажем, я хочу загрузить файл в 100 кусков:
Это кажется очень неинтуитивным и очень быстро читается.
В .Net они решили все это, используя ключевое слово async и wait, в основном разворачивая эти продолжения в кажущийся синхронный поток.
Ответ 4
Вы сказали (в комментарии): "Асинхронные методы предлагают легкую асинхронизацию без использования явных потоков". Но, похоже, ваша жалоба заключается в том, что вы пытаетесь сделать что-то с асинхронными методами, и это нелегко. Вы видите противоречие здесь?
Когда вы используете дизайн, основанный на обратном вызове, вы жертвуете способностью выражать поток управления напрямую с помощью встроенных в язык структур.
Поэтому я предлагаю вам прекратить использовать обратный вызов. 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
имеет только асинхронные методы, а не синхронные методы?"
Мы можем использовать GCD для создания синхронной обертки для асинхронного метода. Нам нужно заставить оболочку вызывать метод async, а затем блокировать до тех пор, пока метод 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;
}
UPDATE
Сначала я отвечу на ваш третий комментарий, а второй второй комментарий.
Третий комментарий
Ваш третий комментарий:
Последнее, но не менее важное: ваше решение посвятить отдельный поток для обертывания синхронной версии вызова более дорогостоящей, чем использование альтернативных асинхронных версий. Thread - дорогостоящий ресурс, и когда он блокирует вас, вы потеряли один поток. Асинхронные вызовы (по крайней мере, в библиотеках ОС) обычно обрабатываются гораздо более эффективным способом. (Например, если вы будете запрашивать 10 URL-адресов одновременно, скорее всего, он не будет разворачивать 10 потоков (или помещать их в threadpool))
Да, использование потока дороже, чем просто использование асинхронного вызова. И что? Вопрос в том, слишком ли это дорого. Objective-C сообщения слишком дороги в некоторых сценариях на текущем оборудовании iOS (например, внутренние петли распознавания лиц в реальном времени или алгоритм распознавания речи), но я не испытываю никаких проблем с их использованием в большинстве случаев.
Действительно ли поток является "дорогостоящим ресурсом", зависит от контекста. Рассмотрим ваш пример: "Например, если вы будете запрашивать 10 URL-адресов одновременно, скорее всего, он не будет разворачивать 10 потоков (или помещать их в threadpool)". Давайте узнаем.
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 использует для реализации этого метода. Я запустил приложение на своем iPhone 4S под управлением iOS 6.0.1, остановился в отладчике и сделал снимок экрана "Навигатор потоков":
![10 NSURLConnection sendAsynchronousRequest: threads]()
Вы можете видеть, что существует 10 потоков с меткой com.apple.root.default-priority
. Я открыл три из них, так что вы можете видеть, что это обычные потоки очереди GCD. Каждый из них вызывает блок, определенный в +[NSURLConnection sendAsynchronousRequest:…]
, который просто поворачивается и вызывает +[NSURLConnection sendSynchronousRequest:…]
. Я проверил все 10, и все они имеют одну и ту же трассировку стека. Итак, на самом деле, в OS-библиотеке имеется 10 потоков.
Я столкнулся с числом циклов от 10 до 100 и обнаружил, что GCD закрывает количество потоков com.apple.root.default-priority
на 64. Поэтому я предполагаю, что остальные 36 запросов, которые я выдал, помещены в очередь в очереди с приоритетом по умолчанию и выиграли 't даже начать выполнение до тех пор, пока не завершится выполнение некоторых из 64 "запущенных" запросов.
Итак, слишком ли дорого использовать поток для превращения асинхронной функции в синхронную функцию? Я бы сказал, это зависит от того, сколько из них вы планируете делать одновременно. Я бы не сомневался, если число до 10 или даже 20.
Второй комментарий
Что приводит меня ко второму комментарию:
Однако, когда у вас есть: делайте эти 3 вещи одновременно, и когда "все" из них закончены, игнорируйте остальных и выполняйте эти 3 вызова одновременно и когда "все" из них заканчиваются,.
Это случаи, когда легко использовать GCD, но мы можем, конечно, комбинировать подходы GCD и async, чтобы использовать меньшее количество потоков, если вы хотите, но при этом используете собственные инструменты для управления потоком.
Сначала мы сделаем typedef для блока завершения удаленного API, просто чтобы сохранить ввод позже:
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
и 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
с помощью dispatch_sync
, а dispatch_sync
всегда запускает блок в текущем потоке, если очередь не является основной очередью.
Мне также понадобится семафор, чтобы разбудить внешнюю функцию, когда какой-либо запрос завершился с статусом передачи или когда все запросы завершены:
dispatch_semaphore_t enoughJobsCompleteSemaphore = dispatch_semaphore_create(0);
Я буду отслеживать количество заданий, которые еще не закончены, и статус последнего задания:
__block int jobsLeft = requestBlocks.count;
__block int outerStatus = 0;
Когда jobsLeft
становится 0, это означает, что либо я установил outerStatus
в состояние, которое проходит тест, либо что все задания завершены. Здесь блок завершения, где я буду работать, отслеживая, буду ли я ждать. Я делаю все это на completionQueue
для сериализации доступа к jobsLeft
и outerStatus
, если удаленный API отправляет несколько блоков завершения параллельно (в отдельных потоках или в параллельной очереди):
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 отмены).