Программирование
Что такое структурированный параллелизм (Structured Concurrency)
Структурированный параллелизм позволяет нам перейти от «ада обратных вызовов» к коду, который легче читать и поддерживать.
Когда мы говорим о параллелизме Swift (Swift Concurrency), мы также часто упоминаем структурированный параллелизм (Structured Concurrency). Это фундаментальная часть async/await в Swift и она помогает нам понять, как работают последние усовершенствования Swift в области параллелизма.
До async/await мы писали наши асинхронные методы, используя замыкания и Grand Central Dispatch (GCD). Это работало хорошо, но часто приводило к так называемому аду замыканий — отсутствию понимания из-за перетекающих друг в друга связанных замыканий. Структурированный параллелизм упрощает выполнение асинхронного кода, но у нас все еще есть неструктурированные задачи. Поэтому пришло время углубиться в фундаментальный аспект параллелизма Swift.
Что означает структурированный параллелизм?
Структурированный параллелизм — это модель, которая упрощает чтение, поддержку и анализ асинхронного кода.
До структурированного параллелизма асинхронный код часто полагался на ад обратных вызовов или вручную управляемые задачи с помощью DispatchQueue
или OperationQueue
. Это приводило к разным потокам выполнения и затруднениям с пониманием порядка выполнения задач.
Благодаря структурированному параллелизму Swift гарантирует, что дочерние задачи остаются в определенном скоупе, что означает:
- Задачи создаются и ожидают действий ясным, структурированным образом — сверху вниз.
- Родительская задача ждет завершения дочерних задач, прежде чем продолжить работу.
- Ошибки автоматически распространяются, что снижает необходимость ручной обработки ошибок между несколькими обработчиками завершения.
Особенно со структурированным параллелизмом упрощается обработка ошибок . У вас больше не будет optional параметров ошибок внутри замыканий или бесконечного разворачивания ошибок, которые бы загромождали бы ваш код.
Пример: структурированный параллелизм в действии
Концепцию структурированного параллелизма лучше всего объяснить на примере. Допустим, мы хотим асинхронно извлечь три фрагмента данных перед отображением результата.
Извлечение данных с помощью замыканий
Без структурированного параллелизма мы бы использовали традиционные обратные вызовы. Это можно сделать например так:
func fetchData(completion: @escaping (String) -> Void) { let seconds = 1.0 // Simulating network delay DispatchQueue.global().asyncAfter(deadline: .now() + seconds) { completion("Data") } } func loadData() { fetchData { data1 in fetchData { data2 in fetchData { data3 in print("Finished loading: \(data1), \(data2), \(data3)") } } } }
Это все еще относительно простые методы, и мы не так много делаем в теле замыканий, но все уже становится довольно загроможденным. Есть несколько проблем с этим подходом:
- Отступ становится глубже с каждым вложенным обратным вызовом (ад обратных вызовов).
- Трудно следовать порядку выполнения.
- Обработка ошибок усложняется.
Мы назвали свойства обратного вызова data1, data2 и data3, что делает немного более очевидным, как будут проходить запросы. Однако без них вы могли бы подумать, что внутренний обратный вызов будет выполняться раньше внешнего. Вдобавок ко всему, что если бы нам нужно было традиционно обрабатывать ошибки внутри колбеков:
func fetchData(completion: @escaping (String, Error?) -> Void) { let seconds = 1.0 // Simulating network delay DispatchQueue.global().asyncAfter(deadline: .now() + seconds) { completion("Data", nil) } } func loadData() { fetchData { data1, error in guard error == nil else { print("Request 1 failed!") return } fetchData { data2, error in guard error == nil else { print("Request 2 failed!") return } fetchData { data3, error in guard error == nil else { print("Request 3 failed!") return } print("Finished loading: \(data1), \(data2), \(data3)") } } } }
Код уже стал довольно сложным. Можно было бы использовать результирующий enum вместо error, но код не улучшится:
func fetchData(completion: @escaping (Result<String, Error>) -> Void) { let seconds = 1.0 // Simulating network delay DispatchQueue.global().asyncAfter(deadline: .now() + seconds) { completion(.success("Data")) } } func loadData() { fetchData { result1 in switch result1 { case .success(let data1): fetchData { result2 in switch result2 { case .success(let data2): fetchData { result3 in switch result1 { case .success(let data3): fetchData { result2 in print("Finished loading: \(data1), \(data2), \(data3)") } case .failure: print("Request 3 failed!") } } case .failure: print("Request 2 failed!") } } case .failure: print("Request 1 failed!") } } }
Честно говоря, мне даже этот пример кода для статьи было трудно написать без ошибок, ха! Очевидно, что должно быть лучшее решение для асинхронного кода — структурированный параллелизм.
Извлечение данных с помощью структурированного параллелизма (async/await)
Взяв последний пример кода, мы можем переписать его с использованием структурированного параллелизма и async/await:
func fetchData() async throws -> String { try await Task.sleep(for: .seconds(1)) // Simulating network delay return "Data" } func loadData() async throws { let data1 = try await fetchData() let data2 = try await fetchData() let data3 = try await fetchData() print("Finished loading: \(data1), \(data2), \(data3)") }
Он читается настолько лучше, что вы почти усомнитесь в реальности происходящего. В этом примере кода есть несколько улучшений:
- Четкий порядок выполнения: код читается сверху вниз — как и синхронный код.
- Проще поддерживать: нет глубокой вложенности обратных вызовов.
- Автоматическое распространение ошибок: если
fetchData()
выдает ошибку, она всплывает естественным образом.
Подробнее об этом читайте в специальной статье об async/await.
А как насчет неструктурированных задач?
Вы могли слышать о неструктурированных задачах в Swift Concurrency. Они действительно существуют, и наиболее распространенным примером является отсоединенная (detached ) задача. Я не буду подробно рассматривать их в этой статье, но на данный момент вот самые важные характеристики неструктурированной задачи:
- Не привязаны к родителю: они существуют независимо и не наследуют автоматически поведение отмены.
- Требуется ручная отмена: разработчикам необходимо явно управлять отменой задач. Больше гибкости, но больше риска: хотя они предлагают больше контроля, они также вносят потенциальные подводные камни, такие как состояние гонки или потерянные задачи.
- Поэтому лучше придерживаться структурированных задач как можно дольше, чтобы вы могли извлечь выгоду из структурированного параллелизма.
Заключение
Структурированный параллелизм позволяет нам перейти от «ада обратных вызовов» к коду, который легче читать и поддерживать. Нет сомнений, что структурированный параллелизм — это будущее, но это также сложная структура с большим количеством концепций, которые нужно охватить. У замыканий были свои недостатки, но и у Swift Concurrency есть свои проблемы!
Спасибо, что прочитали!
-
Программирование3 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков6 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8