[Async/await] Проблемы Swift 5.4

от автора

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

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

Введение

Со времен iOS 2 разработчикам было предложено работать с NSOperation и NSOperationQueue для реализации асинхронных задач. Данная библиотека давала возможность выстраивать цепочку задач и исполнять их на абстрактных очередях, инкапсулирующих работу с потоками. 

В 2009 году Apple представила новую библиотеку — Grand Central Dispatch (GCD), которая также работала с очередями, но была легче в использовании, в связи с уходом от ООП концепции по работе с задачами. Их стало легче создавать, запускать, но стало сложнее выстраивать логику и взаимосвязь между ними.

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

Недостатки Swift ниже 5.5

Прежде чем знакомиться с новой технологией давайте разберёмся с недостатками текущих решений. Начнем по порядку: 

1. Pyramid of doom ?

Разберем небольшой пример, который иллюстрирует данную проблему.

struct User { }  func saveUserToCoreData(user: User, completion: @escaping (User) -> Void) { } func loadUserFromNetworkFromNetwork(for id: String, completion: @escaping (User) -> Void) { } 

У нас есть структура User, которая хранит данные о пользователе, а также реализованы 2 метода:

  1. saveUserToCoreData — сохраняет пользователя в CoreData.

  2. loadUserFromNetwork — загружает данные о пользователе из сети

Последние две метода принимают замыкание, которое вызовется по завершению их работы. 

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

Решение будет выглядеть так:

func fetchUserData(for id: String, completion: @escaping ((User) -> Void)) {     loadUserDataFromNetwork(for: id) { user in         saveToCoreData(user: user) { savedUser in             completion(savedUser)         }     } }  let userID = "testID" fetchUserData(for: userID) {     print("User is \($0))") }

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

2. Error handling

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

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

func saveUserToCoreDataWithError(data: User, completion: @escaping (Result<User, Error>) -> Void) { } func loadUserFromNetworkWithError(for id: String, completion: @escaping (Result<User, Error>) -> Void) { } 

Теперь наши методы работают с Result, необходимо актуализировать метод

func fetchUserDataWithError(for id: String, completion: @escaping ((Result<User, Error>) -> Void)) {     loadUserFromNetworkWithError(for: id) { result in         do {             saveUserToCoreDataWithError(data: try result.get()) { savedResult in                 guard let savedUser = try? savedResult.get() else { return }                 completion(.success(savedUser))             }         } catch {             // completion(.failure(_)) -  Обработка ошибки         }     } } 

В таком случае вложенность еще увеличилась и код стал трудным для восприятия. Также к недостатку можно отнести сам принцип обработки ошибки — он работает благодаря тому, что мы внедрили дополнительный тип данных Result, хотя в Swift уже есть try-catch блоки для обработки ошибок в синхронных функциях. 

3. Many mistakes are easy to make ?

Следующий пункт гласит, что при написании кода с замыканиями легко допустить ошибку.  Надеюсь, что вы уже заметили ее в примере выше, вот же она, на 5-ой строчке: 

guard let savedUser = try? savedResult.get() else { return }  

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

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

4. Недостатки библиотек от Apple ?

Однако помимо проблем у языка Swift, они также присутствуют в представленных Apple библиотеках, а именно: 

GCD

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

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

  3. Сложно выстраивать цепочки из задач, делить ресурсы.

NSOperation

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

  1. Возможна циклическая зависимость задач, что приведет к deadlock.

  2. Трудно написать асинхронную задачу.

5. Работа с потоками

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

  1. У каждого Thread есть свой стек, что дает минус по памяти.

  2. Переключение между ними занимает время. 

  3. Возможна проблема “Thread explosion” — когда  система не справляется с большим количеством потоков и не может грамотно и эффективно управлять ими -> мы получаем крэш.

Выводы

Таким образом, мы вспомнили основные недостатки написания асинхронного кода в Swift ниже 5.5, а также основные недостатки текущих библиотек от Apple.

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


Также подписывайтесь на наш телеграм-канал «Голос Технократии». Каждое утро мы публикуем новостной дайджест из мира ИТ, а по вечерам делимся интересными и полезными мастридами.


ссылка на оригинал статьи https://habr.com/ru/post/668018/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *