Перейти к содержанию

1. Введение


Изучение данного блока предполагает предварительное знание синтаксиса языка Swift. Для успешного освоения этого материала, необходимо иметь базовое понимание синтаксиса языка Swift. Это включает в себя знание основных структур данных, операторов, циклов, функций, абстракций и других ключевых элементов языка. Без этих фундаментальных знаний будет сложно понять более сложные концепции и примеры, которые будут рассматриваться в данном блоке.


Мы начинаем погружаться в мир Structured Concurrency.

С развитием технологий наши телефоны становятся мощнее, в них появляются мощные процессоры, увеличивается количество ядер.

❓ Для чего нужны ядра?

Для того чтобы ускорить выполнение задач. То есть мы понимаем, что если в телефоне 8 ядер - то система одновременно может выполнить 8 задач.

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

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

Основными понятиями в Structured Concurrency - являются задачи и исполнители.

Задачи. Task

  • Родительская задача - большая задача, содержащая в себе множество дочерних подзадач. Если родительская задача отменилась, то и выполнение дочерних задач также должно отмениться. Если дочерняя подзадача выполнилась с ошибкой, то родительская задача должна определить как действовать в этом случае.
  • Дочерняя задача - небольшая задача, которая находится внутри какой-то родительской задачи, она наследует исполнителя родительской задачи.

Исполнители. Actors

Задачи должны всегда иметь исполнителя. Он определяется почти всегда самостоятельно системой в зависимости он того какой код вы написали. Также хочется отметить, что мы можем написать своего исполнителя - Custom Executor - (но это очень сложная задача, которую необходимо внимательно и детально прорабатывать). Существует несколько типов исполнителей:

  1. Main Actor - у этого исполнителя все задачи имеют самый высокий приоритет. Он ОДИН на всё наше приложение. Он отвечает за отрисовку всего нашего интерфейса, за анимации, за переходы между экранами. Поэтому на нём крайне желательно выполнять ТОЛЬКО работу связанную с интерфейсом, если данный исполнитель начнёт выполнять тяжелую работу - то наше приложение будет фризиться. Поэтому такие задачи нужно отдавать на Global Actor
  2. Global Actor - сущность, которая имеет внутри себя несколько исполнителей:

  3. Параллельный исполнитель - такой исполнитель может взять несколько задач и начать их выполнение. Такой исполнитель и позволяет ускорить работу приложения.

  4. Serial исполнитель - такой исполнитель просто выполняет 1 задачу за другой.

  5. Custom исполнитель - мы можем написать своего исполнителя на основе предыдущих

У каждого исполнителя есть определённые инструкции, с помощью которых он работает.

  • Алгоритм взятия в работу задач
  • Способ взаимодействия с другими исполнителями
  • Приоритет
  • и некоторые другие свойства, который мы рассмотрим позже

Такие инструкции называются контекстом исполнителя

Контекст исполнителя

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

⚙ Например пользователь нажал на кнопку и мы отловили это нажатие. Это будет происходить в главном контексте, потому что события нажатия идут из интерфейса приложения за который отвечает Main Actor.

  • Если мы решаем создать дочернюю задачу (Task) - то она будет наследовать контекст MainActor и будет продолжать выполнять код на главном исполнителе

  • Если же мы создадим родительскую задачу (Task.detached) - то она уже не будет наследовать контекст Main Actor. Она будет отдана Global исполнителю, который начнёт её выполнять.

Инверсия приоритетов

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

Исполнители всеми силами борятся с ситуацией когда высоко-приоритетная задача ждёт пока выполнится низко-приоритетная задача. Такая ситуация и называется инверсией приоритетов.

Пример

Мы написали текст в строке и нажали на кнопку отправить. Какие здесь будут дочерние и родительские задачи?

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

Поэтому нам необходимо будет создать родительскую задачу - по отправке сообщения, в которой будет содержаться дочерние задачи. Какие дочерние задачи могут нам потребоваться?

  • Проверка текста на корректность
  • Формирование структуры сообщения из текста
  • Сохранение этой структуры в базу данных
  • Отправка в сеть
  • Обработка результата отправки
  • В случае ошибки - показ ошибки

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

  • Открылся экран и нам нужно загрузить картинку из сети и отрисовать её. Какие здесь будут дочерние и родительские задачи?

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

    Дочерние задачи:

    • Запросить идентификатор картинки, необходимый размер
    • Скачать картинку
    • Сохранить картинку
    • Вырезать её с нужным размером
    • Если на каком-то этапе произошла ошибка - необходимо правильно обработать её и показать пользователю ошибку