Учебное пособие по Grand Central Dispatch для Swift 5: часть 1/2

Узнайте все о многопоточности, очередях отправки и параллелизме в первой части этого руководства по Swift 5 на Grand Central Dispatch.

Примечание об обновлении: Фабрицио Бранкати обновил это руководство для iOS 15, Swift 5.5 и Xcode 13. Эван Дехайсер написал предыдущее обновление, а Кристин Абернати написала оригинал.

Grand Central Dispatch (GCD) — это низкоуровневый API для управления одновременными операциями. Это может помочь повысить скорость отклика вашего приложения, откладывая вычислительно ресурсоемкие задачи в фоновом режиме. Работать с такой моделью параллелизма проще, чем с блокировками и потоками.

В этом руководстве по Grand Central Dispatch, состоящем из двух частей, вы узнаете все тонкости GCD и его Swifty API. В этой первой части объясняется, что делает GCD, и демонстрируется несколько основных функций GCD. Во второй части вы узнаете о некоторых дополнительных функциях, которые может предложить GCD.

Вы будете опираться на существующее приложение под названием GooglyPuff. GooglyPuff — это неоптимизированное, «небезопасное для потоков» приложение, которое накладывает гугливые глаза на обнаруженные лица с помощью API обнаружения лиц Core Image. Вы можете выбрать изображения, к которым нужно применить этот эффект, из своей фотобиблиотеки или загрузить изображения из Интернета.

Ваша миссия — использовать GCD для оптимизации приложения и обеспечения безопасного вызова кода из разных потоков.

Примечание. В Swift 5.5 появилась официальная поддержка асинхронности и ожидания, а также в язык был внесен огромный набор улучшений.

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

В этом руководстве по Grand Central Dispatch вы углубитесь в основные концепции GCD, в том числе:

  • Многопоточность
  • Очереди отправки
  • Параллелизм

Начиная

Используйте «Загрузить материалы» в верхней или нижней части этого руководства, чтобы загрузить начальный проект. Откройте его в Xcode и запустите, чтобы увидеть, с чем вам придется работать.

Примечание. В настоящее время приложение плохо работает в симуляторе iOS — глаза будут появляться в случайных местах, а не над глазами людей. Вы должны использовать реальное устройство, чтобы CIDetector работал.

Начальный экран изначально пуст. Коснитесь +, затем выберите Le Internet, чтобы загрузить предустановленные изображения из Интернета. Коснитесь первого изображения, и вы увидите, как к лицу добавились выпученные глаза.

В этом руководстве вы в основном будете работать с четырьмя классами:

  • PhotoCollectionViewController: начальный контроллер представления. Он отображает выбранные фотографии в виде миниатюр.
  • PhotoDetailViewController: отображает выбранную фотографию из PhotoCollectionViewController и добавляет к изображению выпученные глаза.
  • Photo: этот протокол описывает свойства фотографии. Он предоставляет изображение, миниатюру и соответствующие им статусы. В проект входят два класса, реализующих протокол:

       — DownloadPhoto, который создает экземпляр фотографии из экземпляра URL.

       — AssetPhoto, который создает экземпляр фотографии из экземпляра PHAsset.

  • PhotoManager: управляет всеми объектами Photo.

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

В этой первой части вы поработаете над несколькими улучшениями, в том числе над оптимизацией процесса поиска в Google и обеспечением потокобезопасности PhotoManager.

Разрушение концепций GCD

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

Изучая параллелизм

В iOS процесс или приложение состоит из одного или нескольких потоков. Планировщик операционной системы управляет потоками независимо друг от друга. Каждый поток может выполняться одновременно, но система сама решает, когда и как это происходит.

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

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

GCD построен поверх потоков. Под капотом он управляет общим пулом потоков. С помощью GCD вы добавляете блоки кода или рабочие элементы в очереди отправки, а GCD решает, в каком потоке их выполнять.

Когда вы структурируете свой код, вы обнаружите блоки кода, которые могут выполняться одновременно, а некоторые — нет. Это позволяет использовать GCD для одновременного выполнения.

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

По сути, concurrency — это структура, а parallelism — выполнение

Понимание очередей

Как упоминалось ранее, GCD работает с очередями отправки через класс, удачно названный DispatchQueue. Вы отправляете единицы работы в эту очередь, и GCD выполняет их в порядке FIFO (первым пришел, первым вышел), гарантируя, что первая отправленная задача будет первой запущенной.

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

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

Параллельные очереди позволяют выполнять несколько задач одновременно. Очередь гарантирует запуск задач в том порядке, в котором вы их добавляете. Задачи могут завершаться в любом порядке, и вы не знаете ни времени, которое потребуется для запуска следующей задачи, ни количества задач, выполняемых в любой момент времени.

Так задумано: ваш код не должен полагаться на эти детали реализации.

См. пример выполнения задачи ниже:

Обратите внимание, как быстро одна за другой запускаются Задача 1, Задача 2 и Задача 3. С другой стороны, Задаче 1 потребовалось некоторое время, чтобы начаться после Задачи 0. Также обратите внимание, что хотя Задача 3 началась после Задачи 2, она завершилась раньше Задачи 2.

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

Общие сведения о типах очередей

GCD предоставляет три основных типа очередей:

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

При отправке задач в глобальные параллельные очереди вы не указываете приоритет напрямую. Вместо этого вы указываете свойство класса качества обслуживания (QoS). Это указывает на важность задачи и направляет НОД при определении приоритета задачи.

Классы QoS:

  • User-interactive: это представляет задачи, которые должны быть выполнены немедленно, чтобы обеспечить приятный пользовательский интерфейс. Используйте его для обновлений пользовательского интерфейса, обработки событий и небольших рабочих нагрузок, требующих малой задержки. Общий объем работы, проделанной в этом классе во время выполнения вашего приложения, должен быть небольшим. Это должно работать в основном потоке.
  • User-initiated: пользователь инициирует эти асинхронные задачи из пользовательского интерфейса. Используйте их, когда пользователь ожидает немедленных результатов и задач, необходимых для продолжения взаимодействия с пользователем. Они выполняются в глобальной очереди с высоким приоритетом.
  • Utility: это представляет длительные задачи, обычно с видимым пользователем индикатором выполнения. Используйте его для вычислений, ввода-вывода, работы в сети, непрерывной передачи данных и подобных задач. Этот класс разработан, чтобы быть энергоэффективным. Это отображается в глобальную очередь с низким приоритетом.
  • Background: представляет задачи, о которых пользователь не знает напрямую. Используйте его для предварительной выборки, обслуживания и других задач, которые не требуют взаимодействия с пользователем и не зависят от времени. Это отображается в глобальную очередь фонового приоритета.

Планирование синхронных и асинхронных функций

С помощью GCD вы можете отправлять задачи синхронно или асинхронно.

Синхронная функция возвращает управление вызывающему объекту после завершения задачи. Вы можете синхронно запланировать единицу работы, вызвав DispatchQueue.sync(execute:).

Асинхронная функция немедленно возвращает значение, приказывая запустить задачу, но не дожидаясь ее завершения. Таким образом, асинхронная функция не блокирует текущий поток выполнения от перехода к следующей функции. Вы можете запланировать единицу работы асинхронно, вызвав DispatchQueue.async(execute:).

Управление задачами

Вы уже довольно много слышали о задачах. Для целей этого руководства вы можете рассматривать задачу как замыкание. Замыкания — это автономные, вызываемые блоки кода, которые вы можете хранить и передавать.

Каждая задача, которую вы отправляете в DispatchQueue, является DispatchWorkItem. Вы можете настроить поведение DispatchWorkItem, например его класс QoS или необходимость создания нового отсоединенного потока.

Обработка фоновых задач

Со всеми этими накопленными знаниями GCD пришло время для вашего первого улучшения приложения!

Вернитесь в приложение и добавьте несколько фотографий из своей фототеки или воспользуйтесь опцией Le Internet, чтобы загрузить несколько. Коснитесь фотографии. Обратите внимание, сколько времени требуется для отображения сведений о фотографии. Задержка более заметна при просмотре больших изображений на медленных устройствах.

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

Это похоже на работу для асинхронного DispatchQueue!

Откройте PhotoDetailViewController.swift. Измените viewDidLoad() и замените эти две строки:

guard let overlayImage = image.faceOverlayImageFrom() else {
  return
}
fadeInNewImage(overlayImage)

Со следующим кодом:

// 1
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  guard let overlayImage = self?.image?.faceOverlayImageFrom() else {
    return
  }

  // 2
  DispatchQueue.main.async { [weak self] in
    // 3
    self?.fadeInNewImage(overlayImage)
  }
}

Вот что делает код, шаг за шагом:

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

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

3. Наконец, вы обновляете пользовательский интерфейс с помощью fadeInNewImage(_:), который выполняет плавный переход нового изображения с выпученными глазами.

В двух местах вы добавляете [weak self], чтобы зафиксировать слабую ссылку на себя в каждом замыкании. Если вы не знакомы со списками захвата, ознакомьтесь с этим руководством по управлению памятью.

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

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

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

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

  • Основная очередь: это обычный выбор для обновления пользовательского интерфейса после завершения работы над задачей в параллельной очереди. Для этого вы кодируете одно замыкание внутри другого. Нацеливание на основную очередь и вызов async гарантирует, что эта новая задача будет выполнена через некоторое время после завершения текущего метода.
  • Глобальная очередь: это обычный выбор для выполнения работы, не связанной с пользовательским интерфейсом, в фоновом режиме.
  • Пользовательская последовательная очередь: хороший выбор, если вы хотите последовательно выполнять фоновую работу и отслеживать ее. Это устраняет конкуренцию за ресурсы и условия гонки, поскольку одновременно выполняется только одна задача. Обратите внимание: если вам нужны данные из метода, вы должны объявить другое замыкание, чтобы получить их, или рассмотреть возможность использования синхронизации.

Отсрочка выполнения задачи

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

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

Откройте PhotoCollectionViewController.swift и заполните реализацию для showOrHideNavPrompt():

// 1
let delayInSeconds = 2.0

// 2
DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [weak self] in
  guard let self = self else {
    return
  }

  if !PhotoManager.shared.photos.isEmpty {
    self.navigationItem.prompt = nil
  } else {
    self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
  }

  // 3
  self.navigationController?.viewIfLoaded?.setNeedsLayout()
}

Вот что происходит выше:

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

showOrHideNavPrompt() выполняется в viewDidLoad() и каждый раз, когда ваш UICollectionView перезагружается.

Создайте и запустите приложение. Должна пройти небольшая задержка, прежде чем появится подсказка:

Примечание. Вы можете игнорировать сообщения Auto Layout в консоли Xcode. Все они исходят из iOS и не указывают на ошибку с вашей стороны.

Почему бы не использовать Таймер? Вы можете использовать его, если у вас есть повторяющиеся задачи, которые легче запланировать с помощью таймера. Вот две причины придерживаться asyncAfter() очереди отправки:

  • Один из них — читабельность. Чтобы использовать Timer, вы должны определить метод, а затем создать таймер с селектором или вызовом определенного метода. С помощью DispatchQueue и asyncAfter() вы просто добавляете замыкание.
  • Таймер запланирован для циклов выполнения, поэтому вам также необходимо убедиться, что вы запланировали его для правильного цикла выполнения, а в некоторых случаях и для правильных режимов цикла выполнения. В связи с этим работа с очередями отправки упрощается.

Управление синглтонами

Одиночки так же популярны на iOS, как фотографии кошек в сети.

Одной из частых проблем с синглтонами является то, что они часто не потокобезопасны. Это беспокойство оправдано, учитывая их использование: синглтоны часто используются несколькими контроллерами, одновременно обращающимися к экземпляру синглтона. Ваш класс PhotoManager является одноэлементным, поэтому вам необходимо рассмотреть эту проблему.

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

Есть два случая безопасности потоков, которые следует учитывать:

  • Во время инициализации экземпляра singleton.
  • Во время чтения и записи в экземпляр.

Инициализация оказывается простым случаем из-за того, как Swift инициализирует статические свойства. Он инициализирует статические свойства при первом доступе к ним и гарантирует атомарность инициализации. То есть Swift рассматривает код, выполняющий инициализацию, как критический раздел и гарантирует его завершение до того, как какой-либо другой поток получит доступ к статическому свойству.

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

Откройте PhotoManager.swift, чтобы увидеть, как вы инициализируете синглтон:

final class PhotoManager {
  private init() {}
  static let shared = PhotoManager()
}

Частный инициализатор гарантирует, что единственный экземпляр PhotoManager назначен общему. Таким образом, вам не нужно беспокоиться о синхронизации изменений в вашем фотомагазине между разными менеджерами.

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

Решение проблемы «читатели-писатели»

В Swift любое свойство, объявленное с помощью ключевого слова let, является константой и, следовательно, доступно только для чтения и потокобезопасно. Однако объявите свойство с помощью ключевого слова var, и оно станет изменяемым и не поточно-ориентированным, если тип данных не предназначен для этого. Типы коллекций Swift, такие как Array и Dictionary, не являются потокобезопасными, если они объявлены изменяемыми.

Хотя многие потоки могут без проблем читать изменяемый экземпляр массива одновременно, небезопасно позволять одному потоку изменять массив, пока другой читает его. Ваш синглтон не предотвращает возникновение этого состояния в его текущем состоянии.

Чтобы увидеть проблему, посмотрите на addPhoto(_:) в PhotoManager.swift, который воспроизведен ниже:

func addPhoto(_ photo: Photo) {
  unsafePhotos.append(photo)
  DispatchQueue.main.async { [weak self] in
    self?.postContentAddedNotification()
  }
}

Это метод записи, поскольку он изменяет изменяемый объект массива.

Теперь взгляните на свойство photos, воспроизведенное ниже:

private var unsafePhotos: [Photo] = []
  
var photos: [Photo] {
  return unsafePhotos
}

Метод получения этого свойства называется методом чтения, поскольку он считывает изменяемый массив. Вызывающий получает копию массива и защищен от ненадлежащего изменения исходного массива. Однако это не обеспечивает никакой защиты от вызова одним потоком метода записи addPhoto(_:), в то время как другой поток одновременно вызывает геттер для свойства photos.

Вот почему резервное свойство называется unsafePhotos — если к нему обратиться не в том потоке, вы можете получить странное поведение!

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

Передача по значению приводит к копии объекта, и изменения в копии не повлияют на оригинал. По умолчанию в Swift экземпляры класса передаются по ссылке, а структуры передаются по значению. Встроенные типы данных Swift, такие как Array и Dictionary, реализованы в виде структур.

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

Рассуждения о барьерах для отправки

Это классическая проблема «читатели-писатели» при разработке программного обеспечения. GCD предоставляет элегантное решение для создания блокировки чтения/записи с использованием диспетчерских барьеров. Диспетчерские барьеры — это группа функций, выступающих узким местом последовательного типа при работе с параллельными очередями.

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

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

На приведенной ниже диаграмме показано влияние барьера на различные асинхронные задачи:

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

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

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

Откройте PhotoManager.swift и добавьте частное свойство чуть выше объявления unsafePhotos:

private let concurrentPhotoQueue =
  DispatchQueue(
    label: "com.raywenderlich.GooglyPuff.photoQueue",
    attributes: .concurrent)

Это инициализирует concurrentPhotoQueue как параллельную очередь. Вы устанавливаете метку с описательным именем, которое полезно при отладке. Как правило, вы используете обратное соглашение об именах в стиле DNS.

Затем замените addPhoto(_:) следующим кодом:

func addPhoto(_ photo: Photo) {
  // 1
  concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
    guard let self = self else {
      return
    }

    // 2
    self.unsafePhotos.append(photo)

    // 3
    DispatchQueue.main.async { [weak self] in
      self?.postContentAddedNotification()
    }
  }
}

Вот как работает ваш новый метод записи:

  • Вы отправляете операцию записи асинхронно с помощью барьера. Когда он выполняется, это единственный элемент в вашей очереди.
  • Вы добавляете объект в массив.
  • Наконец, вы публикуете уведомление о том, что добавили фотографию. Вы должны опубликовать это уведомление в основном потоке, потому что оно будет работать с пользовательским интерфейсом. Таким образом, вы асинхронно отправляете другую задачу в основную очередь, чтобы вызвать уведомление.

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

Чтобы обеспечить безопасность потоков при записи, вам необходимо выполнять чтение в concurrentPhotoQueue. Вам нужны возвращаемые данные из вызова функции, поэтому асинхронная диспетчеризация их не урежет. В этом случае синхронизация будет отличным кандидатом.

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

Однако вам нужно быть осторожным. Представьте, что вы вызываете sync и выбираете текущую очередь, в которой вы уже работаете. Это приведет к тупиковой ситуации.

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

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

Вот краткий обзор того, когда и где использовать синхронизацию:

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

По-прежнему в PhotoManager.swift измените метод получения свойств фотографий:

var photos: [Photo] {
  var photosCopy: [Photo] = []

  // 1
  concurrentPhotoQueue.sync {
    // 2
    photosCopy = self.unsafePhotos
  }
  return photosCopy
}

Вот что происходит, шаг за шагом:

  1. Отправка синхронно в concurrentPhotoQueue для выполнения чтения.
  2. Сохраните копию массива фотографий в photosCopy и верните ее.

Создайте и запустите приложение. Скачать фотографии через Le Internet. Он должен вести себя как и раньше, но под капотом у вас есть несколько счастливых нитей.

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

Куда пойти отсюда?

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

В этом руководстве по Grand Central Dispatch вы узнали, как сделать свой код потокобезопасным и поддерживать быстродействие основного потока при выполнении задач, интенсивно использующих ЦП.

Если вы можете повысить минимальную требуемую версию своего приложения до iOS 15, вам также следует проверить, как работает асинхронность и ожидание, с помощью видеоролика WWDC 2021: введение в асинхронность/ожидание. Для более глубокого погружения ознакомьтесь с нашей книгой Modern Concurrency in Swift.

Если вы планируете оптимизировать свои собственные приложения, вам следует профилировать свою работу с помощью встроенного в Xcode Time Profiler. Использование этого инструмента выходит за рамки этого руководства, поэтому ознакомьтесь с разделом «Как использовать инструменты» для получения отличного обзора.

Наш видеокурс iOS Concurrency with GCD and Operations также охватывает множество тем, затронутых в этом руководстве.

Во второй части этого руководства вы еще глубже погрузитесь в API GCD, чтобы делать еще больше интересных вещей.

Если у вас есть какие-либо вопросы или комментарии, не стесняйтесь присоединиться к обсуждению ниже!

Ссылка на материалы