Основной поток против фонового потока. Async/await и Актер. GCD против OperationQueue. Групповая отправка, как усилить фоновый поток и многое другое

Введение

В этой статье мы узнаем следующее:

TABLE OF CONTENTS
What Is Multithreading
  Serial Queue vs. Concurrent Queue
  Parallelism
  Concurrency
Basics of Multithreading
  Main Thread (UI Thread) vs. Background Thread (Global Thread)
GCD (Grand Central Dispatch)
DispatchGroup
DispatchSemaphore
DispatchWorkItem
Dispatch Barrier
AsyncAfter
(NS)Operation and (NS)OperationQueue
DispatchSource (How To Handle Files and Folders)
Deadlock (Issue What To Avoid)
Main Thread Checker (How To Detect Thread Issues)
Threads in Xcode (How To Debug Threads)
Async / Await / Actor Is iOS13+

Я знаю, что есть много тем. Если что-то вам уже знакомо, пропускайте это и читайте неизвестные части. Есть хитрости и советы.

Многопоточность в реальных примерах

Представьте, что у вас есть ресторан. Официант собирает заказы. Кухня готовит еду, бармен варит кофе и коктейли.

В какой-то момент многие люди будут заказывать кофе и еду. Это требует больше времени, чтобы подготовиться. Внезапно раздается звонок о том, что в 5 раз больше еды и в 4 раза больше кофе. Официанту нужно будет обслуживать столики один за другим, даже если все продукты приготовлены. Это последовательная очередь. С последовательной очередью вы ограничены только одним официантом.

Теперь представьте, что есть два или три официанта. При этом они могут обслуживать столы намного быстрее. Это Параллелизм. Использование нескольких процессоров для работы нескольких процессов.

Теперь представьте, что один официант не будет обслуживать один стол сразу, а сначала подаст все кофе ко всем столам. Затем они попросят заказать некоторые из новых столов, а затем подадут всю еду. Эта концепция называется Параллелизм. Это переключение контекста, управление и выполнение множества вычислений одновременно. Это не обязательно означает, что они когда-либо будут работать одновременно. Например, многозадачность на одноядерной машине.

Все современные устройства имеют несколько процессоров (центральный процессор). Чтобы иметь возможность создавать приложения с бесшовными потоками, нам необходимо понимать концепцию многопоточности. Это способ, как мы будем обрабатывать некоторые задачи в приложении. Важно понимать, что если что-то работает, возможно, не лучшим, желаемым образом. Много раз я вижу длительную задачу, которая выполняется в потоке пользовательского интерфейса и блокирует выполнение приложения на пару секунд. В настоящее время это может быть невозможный момент. Пользователи могут удалить ваше приложение, так как считают, что другие приложения запускаются быстрее, быстрее загружают книги, музыку. Конкуренция большая, и требования высокие.

Вы можете увидеть задачу «Serial 6», если она запланирована на текущее время. Он будет добавлен в список в порядке FIFO и ожидает выполнения в будущем.

Основы многопоточности

Одна вещь, с которой я боролся, это отсутствие стандартной терминологии. Чтобы помочь с этим для этих тем, я сначала запишу синонимы, примеры. Если вы пришли из какой-то другой технологии, кроме iOS, вы все равно можете понять концепцию и перенести ее, поскольку основы те же. Мне повезло, что в начале своей карьеры я работал с C, C++, C#, node.js, Java (Android) и т. д., поэтому я привык к этому переключению контекста.

  • Основной поток/поток пользовательского интерфейса: это поток, который запускается вместе с приложением, предварительно определенный последовательный поток. Он прослушивает взаимодействие с пользователем и изменения пользовательского интерфейса. На все изменения немедленно нужна реакция. Будьте осторожны, чтобы не добавлять в эту ветку огромную работу, так как приложение может зависнуть.

DispatchQueue.main.async {
    // Run async code on the Main/UI Thread. E.g.: Refresh TableView
}
  • Фоновая цепочка (глобальная): предопределена. В основном мы создаем задачи в новых потоках исходя из наших потребностей. Например, если нам нужно загрузить какое-то изображение большого размера. Это делается в фоновом потоке. Или любой вызов API. Мы не хотим запрещать пользователям ждать завершения этой задачи. Мы вызовем вызов API для получения списка данных о фильмах в фоновом потоке. Когда он прибывает и выполняется синтаксический анализ, мы переключаемся и обновляем пользовательский интерфейс в основном потоке.

DispatchQueue.global(qos: .background).async {
     // Run async on the Background Thread. E.g.: Some API calls.
}

На картинке выше мы добавили точку останова в строку 56. Когда она срабатывает и приложение останавливается, мы можем видеть это на панели слева от потоков.

  1. Вы можете увидеть имя файла DispatchQueue(label: “com.kraken.serial”). Метка является идентификатором.
  2. Эти кнопки могут быть полезны для отключения/фильтрации вызовов системных методов, чтобы видеть только вызовы, инициированные пользователем.
  3. Вы можете видеть, что мы добавили sleep(1). Это останавливает выполнение кода на 1 секунду.
  4. И если вы следите за порядком, он все равно срабатывает последовательно.

На основе предыдущей версии iOS одним из двух наиболее часто используемых терминов является последовательная очередь и параллельная очередь.

  1. Это результат одного из Concurrent Queue. Вы также можете увидеть выше серийный/основной поток (com.apple.main-thread).
  2. sleep(2) добавляется к этой точке.
  3. Вы видите, что нет никакого порядка. Это было завершено асинхронно в фоновом потоке.
let mainQueue = DispatchQueue.main
let globalQueue = DispatchQueue.global()
let serialQueue = DispatchQueue(label: “com.kraken.serial”)
let concurQueue = DispatchQueue(label: “com.kraken.concurrent”, attributes: .concurrent)

Мы также можем создать частную очередь, которая также может быть последовательной и параллельной.

GCD (Гранд Сентрал Диспетчер)

GCD — это низкоуровневый многопоточный интерфейс Apple для поддержки параллельного выполнения кода на многоядерном оборудовании. Простым способом GCD позволяет вашему телефону загружать видео в фоновом режиме, сохраняя при этом отзывчивость пользовательского интерфейса.

«DispatchQueue — это объект, который управляет выполнением задач последовательно или одновременно в основном потоке вашего приложения или в фоновом потоке». — Разработчик Apple

Если вы заметили в приведенном выше примере кода, вы можете увидеть «qos». Это означает качество обслуживания. С помощью этого параметра мы можем определить приоритет следующим образом:

  • background — мы можем использовать это, когда задача не зависит от времени или когда пользователь может выполнять какие-либо другие действия, пока это происходит. Например, предварительная загрузка некоторых изображений, загрузка или обработка некоторых данных в этом фоне. Эта работа занимает значительное время, секунды, минуты и часы.
  • утилита — долгоиграющая задача. Некоторые обрабатывают то, что может видеть пользователь. Например, загрузка некоторых карт с индикаторами. Когда задача занимает пару секунд, а в итоге пару минут.
  • userInitiated — когда пользователь запускает какую-либо задачу из пользовательского интерфейса и ждет результата, чтобы продолжить взаимодействие с приложением. Эта задача занимает пару секунд или мгновение.
  • userInteractive — когда пользователю нужно немедленно завершить какую-то задачу, чтобы иметь возможность перейти к следующему взаимодействию с приложением. Мгновенная задача.

Полезно также пометить файл DispatchQueue. Это может помочь нам идентифицировать поток, когда он нам понадобится.

DispatchGroup

Часто нам нужно запустить несколько асинхронных процессов, но нам нужно только одно событие, когда все будут завершены. Этого можно достичь с помощью DispatchGroup.

«Группа задач, которые вы отслеживаете как единое целое», — Apple Docs.

Например, иногда вам нужно сделать несколько вызовов API в фоновом потоке. Прежде чем приложение будет готово к взаимодействию с пользователем или к обновлению пользовательского интерфейса в основном потоке. Вот код:

  • Шаг 1. Создайте DispatchGroup
  • Шаг 2. Затем для этой группы необходимо вызвать событие group.enter() для каждой запущенной задачи.
  • Шаг 3. Для каждого group.enter() необходимо вызвать также group.leave(), когда задача будет завершена.
  • Шаг 4. Когда все пары ввода-вывода завершены, вызывается group.notify. Если вы заметили, что это делается в фоновом потоке. Вы можете настроить в соответствии с вашими потребностями.

Стоит упомянуть опцию wait(timeout:). Он подождет некоторое время, пока задача завершится, но после истечения времени ожидания продолжится.

DispatchСемафор

«Объект, который управляет доступом к ресурсу в нескольких контекстах выполнения с помощью традиционного счетного семафора». — Документы Apple

Вызывать wait() каждый раз при доступе к какому-либо общему ресурсу.

Позвоните signal(), когда мы будем готовы освободить общий ресурс.

value в DispatchSemaphore указывает количество одновременных задач.

DispatchWorkItem

Распространено мнение, что когда задача GCD запланирована, ее нельзя отменить. Но это не правда. Это было верно до iOS8.

«Работа, которую вы хотите выполнить, инкапсулирована таким образом, что позволяет прикрепить дескриптор завершения или зависимости выполнения». — Документы Apple

Например, если вы используете строку поиска. Каждый набор букв вызывает вызов API, чтобы запросить у сервера список фильмов. Итак, представьте, что вы печатаете «Бэтмен». «Б», «Ба», «Летучая мышь»… каждая буква будет вызывать сетевой вызов. Мы не хотим этого. Мы можем просто отменить предыдущий вызов, если, например, в течение этого односекундного диапазона будет набрана другая буква. Если время проходит одну секунду, а пользователь не набирает новую букву, то мы считаем, что нужно выполнить вызов API.

Конечно, используя функциональное программирование, такое как RxSwift / Combine, у нас есть лучшие варианты, такие как debounce(for:scheduler:options:).

Диспетчерский барьер

Dispatch Barriers решает проблему с блокировкой чтения/записи. Это гарантирует, что будет выполнен только этот DispatchWorkItem.

«Это делает небезопасные для потоков объекты потокобезопасными». — Документы Apple

Например, если мы хотим сохранить игру, мы хотим записать в какой-то открытый общий файл, ресурс.

АсинкАфтер

Мы можем использовать этот код для задержки выполнения некоторых задач:

С моей точки зрения, это источник всего зла, если не считать исключений. Для каждой асинхронной задачи, которой нужна задержка, я бы предложил подумать об этом и, если возможно, использовать какую-либо систему управления состоянием. Не выбирайте этот вариант в качестве первого выбора. Обычно есть другой способ.

(NS)Операция и (NS)Очередь операций

Если вы используете NSOperation, это означает, что вы используете GCD под поверхностью, поскольку NSOperation построен поверх GCD. Некоторые преимущества NSOperation заключаются в том, что он имеет более удобный интерфейс для зависимостей (выполняет задачу в определенном порядке), он Observable (KVO для наблюдения за свойствами), имеет Pause, Cancel, Resume и Control (вы можете указать количество задач в очереди).

Вы можете установить счетчик одновременных операций равным 1, чтобы он работал как последовательная очередь.

queue.maxConcurrentOperationCount = 1

Это последняя группа отправки. Разница лишь в том, что писать сложные задачи гораздо проще.

DispatchSource

DispatchSource используется для обнаружения изменений в файлах и папках. Он имеет множество вариаций в зависимости от наших потребностей. Я просто покажу один пример ниже:

Тупик

Бывает ситуация, когда две задачи могут ждать завершения друг друга. Это называется тупик. Задача никогда не будет выполнена и заблокирует приложение.

Никогда не вызывайте задачи синхронизации в основной очереди; это вызовет взаимоблокировку.

Проверка основного потока

Есть способ получить предупреждение о том, что мы сделали что-то не так. Это действительно полезная опция, и я рекомендую ее использовать. Он может легко обнаружить некоторые нежелательные проблемы.

Если вы откроете цель и отредактируете схему, как на следующем изображении, включите средство проверки основного потока, тогда, когда мы выполним какое-либо обновление пользовательского интерфейса в фоновом режиме, эта опция во время выполнения уведомит нас. См. изображение ниже для фиолетового уведомления:

Вы также можете увидеть в терминале Xcode, что не так. Для новичков это может быть немного странным сообщением, но вы быстро к нему привыкнете. Но вы можете подключить, что внутри этой строки есть имя метода, в котором проблема.

Потоки в Xcode

Во время отладки есть пара трюков, которые могут нам помочь.

Если вы добавите точку останова и остановитесь на какой-то строке. В терминале Xcode вы можете ввести команду thread info. Она распечатает некоторые детали текущего потока.

Вот еще несколько полезных команд для терминала:

po Thread.isMainThread

po Thread.isMultiThreaded()

po Thread.current

po Thread.main

Возможно, у вас была похожая ситуация — когда приложение вылетало и в логе ошибок было что-то вроде com.alamofire.error.serialization.response. Это означает, что фреймворк создал какой-то пользовательский поток, и это идентификатор.

Асинхронно / Ожидание

С iOS13 и Swift 5.5 были представлены долгожданные Async/Await. Со стороны Apple было приятно, что они признали проблему, заключающуюся в том, что когда появляется что-то новое, возникает длительная задержка, прежде чем это можно будет использовать в производстве, поскольку нам обычно требуется поддержка большего количества версий iOS.

Async/Await — это способ запуска асинхронного кода без обработчиков завершения.

Вот код, который стоит упомянуть:

  • Task.isCancelled
  • Task.init(priority: .background) {}
  • Task.detached(priority: .userInitiated) {}
  • Task.cancel()

Я бы выделил TaskGroup. Это DispatchGroup в мире Await/Async. Я обнаружил, что у Пола Хадсона есть действительно хороший пример по этой ссылке.

Актер

Актеры — это классы, ссылочные типы, которые являются потокобезопасными. Они решают проблемы гонки данных и параллелизма. Как вы можете видеть ниже, доступ к свойству актора осуществляется с помощью ключевого слова await.

«Актеры позволяют только одной задаче получить доступ к своему изменяемому состоянию за раз». — Документы Apple

Заключение

Мы рассмотрели множество тем многопоточности — от пользовательского интерфейса и фонового потока до взаимоблокировок и DispatchGroup. Но я уверен, что вы сейчас на пути к тому, чтобы стать экспертом или, по крайней мере, подготовиться к вопросам интервью iOS по темам многопоточности.

Весь пример кода можно найти по следующей ссылке: GitHub. Я надеюсь, что будет полезно поиграть с ним самостоятельно.

Если вы дошли до этого места, спасибо за чтение. Вы заслуживаете кофе ☕️. 🙂 Если вам нравится контент, пожалуйста 👏, поделитесь и подпишитесь, это значит для меня. Если у вас есть предложения или вопросы, пожалуйста, не стесняйтесь комментировать.

Хотите связаться?
Вы можете связаться со мной в LinkedIn, Twitter или на https://skyspiritlabs.com/. Есть больше статей и руководств.