Я провожу эксперимент на основе этого ответа: https://stackoverflow.com/a/17921058/767653
Это мой минимальный воспроизводимый код:
- (void)viewDidLoad {
[super viewDidLoad];
__block BOOL completed = NO;
[self doMyWork:^{
completed = YES;
}];
while (!completed) {
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
}
}
Насколько я понимаю, этот код зашел в тупик. Поскольку блок завершения запускается в основном потоке, для того, чтобы блок завершения был запущен, viewDidLoad должен вернуться. И чтобы viewDidLoad вернулся, необходимо запустить блок завершения для переключения флага completed (следовательно, циклическое ожидание).
Однако, когда я запускаю его, в этом коде нет тупика. Где я неправильно понял?
Как можно приостановить выполнение viewDidLoad, находясь в середине? Я думал, вам нужно выполнить эту функцию, чтобы основная очередь могла получить следующую задачу. Или я неправильно понимаю? И что я сделал иначе, чем по ссылке, которую предоставил?
Замените [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; на Thread.sleep(1) и посмотрите, что произойдет. В этом разница между блокировкой потока и задержкой возврата viewDidLoad, но при этом выполнением RunLoop. Ваш текущий код заблокировал обновления пользовательского интерфейса, и это плохая идея, но сам поток не заблокирован, поскольку вы передали выполнение обратно в RunLoop.
У меня могут возникнуть проблемы с пониманием «перенес выполнение обратно в цикл выполнения». Можно ли приостановить выполнение функции (а затем выполнить другую функцию) без переключения потока? У меня всегда сложилось впечатление, что выполнение функции можно приостановить при переключении потока (так называемое переключение контекста потока), но я не знал, что это также возможно в пределах одного потока. это удивительно.
если вы позвоните someOtherFunction() в viewDidLoad, выполнение будет приостановлено? Нет, вы перенесли его на someOtherFunction. В вашем случае вы передали его в runUntilDate, и эта функция продолжит выполнять задачи в цикле выполнения.
В каждом потоке есть только один стек вызовов. После вызова someOtherFunction запись активации (или «кадр») viewDidLoad все еще находится в стеке вызовов, а новая запись для someOtherFrame просто располагается поверх нее. В моем примере это возможно только в том случае, если runUntilDate фактически вызывает мой блок завершения. Я правильно понимаю?
Косвенно, да. Ваш блок завершения находится в очереди таймера цикла выполнения. Поскольку вы поддерживаете RunLoop в рабочем состоянии, он в конечном итоге вызовет ваш обработчик завершения. Как я уже сказал, вместо этого используйте Thread.sleep, и вы увидите взаимоблокировку, потому что вы заблокируете поток и предотвратите запуск RunLoop.
«В каждом потоке есть только один стек вызовов»; это тоже не совсем так. Модель диспетчеризации в iOS намного сложнее, чем простые потоки. У вас есть большие центральные очереди отправки, которые сопоставлены с потоками и циклами выполнения, которые управляют отправляемой работой.
Каким бы причудливым ни был GCD, он все равно построен на основе потоков. Это означает, что в каждом потоке может быть только один стек вызовов. Итак, в моем примере, если runUntilDate не вызывает мое закрытие, то это наверняка приведет к тупику.
Давайте продолжим обсуждение в чате.





Ваш [NSRunLoop.currentRunLoop runUntilDate... выполняет работу основного цикла, поэтому взаимоблокировки не происходит.
Это секретная техника преобразования асинхронных функций в синхронные. Но использовать его следует только в экстренных случаях, когда преобразование кода в асинхронный невозможно.
Вы сказали:
Насколько я понимаю, этот код зашел в тупик. Поскольку блок завершения запускается в основном потоке, для того, чтобы блок завершения был запущен,
viewDidLoadдолжен вернуться. И чтобыviewDidLoadвернулся, необходимо запустить блок завершения, чтобы переключить флаг завершения (следовательно, циклическое ожидание).
Да и нет.
Технически это не тупик, поскольку цикл выполнения все еще обрабатывает события. Ваш пользовательский интерфейс может показаться адаптивным. Но поскольку viewDidLoad не возвращается, некоторые другие события (например, viewDidAppear) вызываться не будут. Будут и другие последствия. Этот код мешает нормальной последовательности событий пользовательского интерфейса, но технически не является «тупиком».
Итог: вы должны разрешить viewDidLoad (и всем событиям UIKit) быстро возвращаться. Итак, либо отправьте это асинхронно (но избегайте использования одной и той же последовательной очереди как для dispatch_async, так и для dispatch_after), или, что лучше, вообще потеряйте это вращение в цикле выполнения.
В наши дни вращение на основной петле почти всегда является ошибкой. GCD предлагает красивые современные решения. Однако, чтобы ответить на этот более широкий вопрос, нам нужно больше подробностей о том, почему вы вообще вращаетесь.
Вы не заблокировали основную тему, потому что ни разу не вызывали блокировку
wait.RunUntilDateпредотвратит дальнейшее выполнение внутриviewDidLoad, но основной цикл выполнения все еще обрабатывается. Вы наверняка сделали пользовательский интерфейс своего приложения не отвечающим на запросы в течение трех секунд. Для возникновения взаимоблокировки вам нужны три вещи: взаимное исключение, отсутствие приоритета и удержание и ожидание. В этом случае у вас нет «удержания и ожидания».