Компьютеры не могут жить без многопоточности и параллелизма. Вы можете использовать Grand Central Dispatch (GCD) в Swift, чтобы ваше приложение одновременно выполняло несколько задач. Grand Central Dispatch имеет первостепенное значение для обеспечения отзывчивости вашего приложения благодаря плавной плавной анимации и переходам.
Многопоточность и параллелизм
Вот как вы можете выполнить задачу асинхронно в Swift:
DispatchQueue.global(qos: .userInitiated).async { // }
Что означает «асинхронный»? Чтобы ответить на этот вопрос, нам нужно сначала обсудить многопоточность .
У iPhone есть процессор (Central Processing Unit). Технически, CPU может выполнять только одну операцию за раз – один раз за такт. Многопоточность позволяет процессору создавать параллельные потоки, между которыми он может переключаться, поэтому несколько задач могут выполняться одновременно.
Чтобы два потока выполнялись одновременно, процессор должен быстро переключаться между их выполнением. Как пользователь смартфона или настольного компьютера, вы не замечаете переключений, потому что они происходят очень быстро. Выполнение нескольких задач одновременно называется параллелизмом.
Типичным примером является поток пользовательского интерфейса. Вы когда-нибудь замечали, как приложения могут выглядеть отзывчивыми и иметь плавную анимацию, даже если процессор сильно загружен? Это происходит из-за многопоточности.
На iPhone пользовательский интерфейс остается отзывчивым и плавным, когда вы загружаете файл из интернета или выполняете сложную задачу. Это связано с тем, что iPhone переключается между рисованием экрана и быстрой загрузкой файла, пока обе задачи не будут выполнены.
Что насчет многоядерных процессоров? Такой ЦП, часто называемый System-on-a-Chip (SoC) и имеет несколько ЦП в одном. Это обеспечивает действительно параллельную обработку вместо параллелизма.
Подводя итог, можно сказать, что многопоточность позволяет процессору быстро переключаться между несколькими задачами таким образом, что создается впечатление, что задачи выполняются одновременно.
Выполнение кода в фоновом режиме
При написании кода приложений многопоточность часто используется как синоним фоновой обработки.
Как разработчик приложения, вы хотите, чтобы ваше приложение всегда оставалось отзывчивым. Допустим, вы отправляете обновление обратно в облачную базу данных и сохраняете заметку, которую пользователь добавил в ваше приложение.
Вы не хотите, чтобы пользовательский интерфейс зависал при сохранении этой заметки, поэтому вы используете фоновую обработку, чтобы делать две вещи одновременно. Несмотря на то, что это действительно многопоточность, вы называете это фоновой обработкой, потому что одна задача остается на переднем плане (пользовательский интерфейс), а одна задача отправляется в фоновый режим (сохранение данных).
var note = Note() note.saveInBackground { var alert = "Ваша заметка сохранена!" alert.show() }
В приведенном выше примере с псевдокодом мы создаем объект note, который сохраняется в фоновом режиме. Когда он сохранен, выполняется замыкание, которое показывает пользователю диалоговое окно с предупреждением.
Функция saveInBackground() будет использовать некоторую форму параллелизма. В Cocoa Touch SDK есть несколько вариантов параллелизма, из которых Grand Central Dispatch (GCD) является наиболее распространенным.
Не блокируйте поток пользовательского интерфейса!
Вы не можете обновить пользовательский интерфейс приложения вне основного потока. Операции пользовательского интерфейса, такие как показ диалога или обновление текста на кнопке, могут выполняться только в главном потоке.
Для этого есть ряд причин, но главная причина – избегать состояния гонки. Состояние гонки возникает, когда две задачи должны последовательно выполняться. Рисование пользовательского интерфейса на экране iPhone является такой же последовательной задачей.
Что если вы выполните синхронную блокирующую задачу в основном потоке пользовательского интерфейса? Поток будет ждать, пока синхронная задача не будет выполнена. В результате пользовательский интерфейс ваших приложений может зависнуть или перестать отвечать на запросы в течение некоторого времени.
Из этого можно сделать несколько выводов:
- Держите поток пользовательского интерфейса свободным, чтобы ваше приложение свободно реагировало на действия пользователя.
- Избегайте блокировки потока пользовательского интерфейса синхронным кодом.
Хорошей новостью является то, что мы можем решить две проблемы с помощью с Grand Central Dispatch! Мы переместим некоторую задачу в фоновый режим, а затем вернем ее результат в основной поток для обновления пользовательского интерфейса.
Выполнение асинхронного кода с Grand Central Dispatch
Когда вам нужны задачи, выполняемые одновременно, стоит ли вам создавать кучу потоков? К счастью, нет. iOS имеет потрясающий механизм для работы с параллельными задачами, который называется Grand Central Dispatch.
Название «Grand Central Dispatch» является ссылкой на Grand Central Terminal в центре Нью-Йорка. Представьте себе процессор как связку железных дорог. Диспетчер перемещает вагоны вдоль железных дорог, чтобы быстро добраться до пункта назначения по ограниченному количеству линий.
Grand Central Dispatch – это оболочка низкоуровневого кода для создания потоков и управления процессами. Акцент делается на диспетчеризации, то есть на обеспечении того, чтобы ряд задач разной важности и продолжительности выполнялись в максимально разумные сроки.
Давайте посмотрим на пример:
DispatchQueue.global(qos: .userInitiated).async { // Загружаем файл DispatchQueue.main.async { // Обновляем UI } }
В приведенном выше примере асинхронная задача отправляется с async(). Код, который выполняется асинхронно, записывается в первом наборе волнистых скобок { }. Это замыкание, которое выполняется в фоновом режиме.
Асинхронная задача отправляется в очередь операций и получает приоритет качества обслуживания. Очереди помогают нам планировать задачи друг за другом, и приоритет QoS указывает, какие задачи важнее завершить в ближайшее время.
Когда асинхронная задача завершается, async() делает еще один вызов для выполнения другой задачи в главном потоке. Как правило, вы не можете обновить пользовательский интерфейс своего приложения вне основного потока, поэтому для этого и нужно замыкание внутри кода.
Качество обслуживания (QoS) и Grand Central Dispatch
Давайте посмотрим на наш код еще раз:
DispatchQueue.global(qos: .userInitiated).async { ··· }
Функция global(qos:) вызывается классом DispatchQueue. Эта функция возвращает ссылку на глобальную очередь, которую мы вызываем через .async(_:). Функция global(qos:) принимает параметр: качество обслуживания (Quality-of-Service – QoS) или «приоритет».
По сути, QoS сообщает диспетчеру, насколько важна поставленная задача. Более важные задачи выполняются ранее в очереди, поэтому имеют больше доступных системных ресурсов и, таким образом, выполняются быстрее, чем та же задача с более низким приоритетом.
Задачи с более высоким QoS потребляют больше энергии аккумулятора, поэтому определение правильного QoS гарантирует, что ваше приложение использует энергию и память максимально эффективно. Избегайте желания пометить каждую задачу как «важную», потому что все задачи будут помечены как одинаково важные и, таким образом, вся система приоритетов будет сведена на нет.
iOS будет придавать задачам с более высоким QoS более высокий приоритет, но также будет регулировать задачи в зависимости от других аспектов среды ОС.
В iOS есть четыре уровня качества обслуживания, от самого высокого до самого низкого:
- .userInteractive – предназначен для интерактивных задач пользователя, таких как анимация и обновление пользовательского интерфейса. Как правило, используйте этот QoS для задач, которые активно использует пользователь вашего приложения .
- .userInitiated – предназначен для задач, которые не позволяют пользователю использовать ваше приложение, например, для сохранения файла. Используйте его для задач, выполнение которых ожидает пользователь вашего приложения.
- .utility – предназначен для задач, которые не требуют немедленного результата, таких как фоновые загрузки с индикатором выполнения. Используйте его для фоновых задач, которые требуют баланс между отзывчивостью, производительностью и энергоэффективностью.
- .background – предназначен для задач, которые не видны пользователю вашего приложения, таких как индексирование, синхронизация и резервное копирование. Этот параметр определяет приоритет эффективности использования энергии.
Вы также можете использовать вариант .default, после чего информация QoS будет выводиться из контекста вашего кода.
Очереди и Grand Central Dispatch
Вернемся к нашему коду:
DispatchQueue.global(qos: .userInitiated).async { ··· DispatchQueue.main.async { ··· } }
Мы видим два вызова async(). Первый в глобальной очереди (DispatchQueue.global), а второй в главной очереди (DispatchQueue.main).
Очередь отправки – это просто пул задач. Grand Central Dispatch определит, какие задачи будут выполнены и также определит, сколько процессорного времени получают эти задачи. Больше процессорного времени означает более быстрое завершение.
Вспомните диспетчера терминала. Какой должен добраться до пункта назначения быстрее всего? Диспетчер может перемещать только один поезд за раз, а перемещение одного поезда означает задержку всех остальных машин.
Диспетчер принимает обоснованное решение и позволяет всем задачам выполняться до их завершения, отдавая предпочтение более важным задачам (QoS). Он также назначает задачи различным процессорам – например, iPhone 11 Pro имеет многоядерную SoC с 6 процессорами. Выполните две задачи на двух ядрах процессора, и они будут выполняться параллельно.
Наконец, в приведенном выше примере другая задача отправляется в основной поток. Например, вы должны сообщить пользователю, что его загрузка завершена. Это то, что вам нужно сделать в главном потоке.
Задачи в очереди на отправку выполняются одна за другой. В конце концов, когда мы выстраиваемся в очередь в супермаркете или кафе-мороженом, каждый из нас обслуживается один за другим. Тем не менее, очередь имеет механизм приоритезации, основанный на качестве обслуживания. Фоновая задача менее важна, чем интерактивная, поэтому интерактивная задача получает больше ресурсов процессора и быстрее завершается.
К счастью, вам не нужно управлять всеми этими приоритетами, очередями, потоками и задачами самостоятельно. Вместо это сделает за вас Grand Central Dispatch.
Задержка выполнения задач с помощью asyncAfter()
В Grand Central Dispatch есть несколько вариантов одновременного выполнения: async() и asyncAfter(). Последний позволяет отложить выполнение задачи в будущем:
let delay = DispatchTime.now() + .seconds(60) DispatchQueue.main.asyncAfter(deadline: delay) { // }
В приведенном выше примере определена константа delay. Ее значение – это момент времени относительно часов по умолчанию. Затем функция asyncAfter(deadline:) просто берет этот интервал и выполняет код, когда достигается интервал задержки, то есть через 60 секунд.