Pull to refresh
12
0
Влад Яндола @kymacat

Ментально пожилой iOS разработчик

Send message

Спасибо за фидбэк)

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

В моем примере контроллер - единственный экран приложения и он сразу помещается в иерархию, поэтому проблемы с этим не будет. Тем не менее, в среднем случае лучше задавать их в viewDidLoad (так как неизвестно как контроллер будет в дальнейшем использоваться). Поправил этот момент в примере. Спасибо за замечание!

Привет. Наверное не совсем тема статьи, но почему опасно? В init с констрейнтами могут быть проблемы в случае если иерархия вьюх грузится из сторибордов и ксибов. Тут я их не использую, все вьюхи создаются программно и добавляются в иерархию перед применением констрейнтов

Насколько я понял вы говорите про функцию ниже (поправьте если я не так понял)

func loadAndProcessImage(from url: URL) async throws -> UIImage {
  let image = try await loadImage(from: url)
  let blurredImage = try await applyBlurFilter(to: image)
  try await cacheToDisk(blurredImage)
  return blurredImage
}

Тут нет обработки ошибок с помощью catch, так как подразумевается что обработка будет происходить в месте вызова данной функции (она сама по себе throws)

При работе с async/await, непосредственно взаимодействие с потоками осуществляется через executor. Executor представляет собой сервис, который принимает задачи и выполняет их на определенных потоках. Каждая Task (или асинхронная функция) работает с определенным экзекьютором. У MainActor executor выполняет все задачи на main потоке.

Код внутри таски из примеров выполняется экзекьютором MainActor’а, следовательно на main потоке.

Из таски в примерах дергаются свойства (computed property) класса ImageLoader. У него нет привязки к @MainActor. Асинхронные задачи из этого класса будут выполняться не на main, так как запускаются другим дефолтным экзекьютором. Поэтому уже до вызова методов URLSession внутри ImageLoader код будет выполняться не в main потоке.

После возврата значения из ImageLoader, таска из viewDidLoad продолжает выполяняться на main, так как после приостановки запускается на экзекьюторе MainActor'а

Можно посмотреть вот на таком примере:

class SomeClass {
  func someFunc() async throws {
    for _ in 0 ..< 5 {
      // 3
      print(Thread.current)
      try await Task.sleep(nanoseconds: 1_000_000)
    }
  }
}

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()

    Task {
      // 1
      print("viewDidLoad", Thread.current)
      // 2
      try await SomeClass().someFunc()
      // 4
      print("viewDidLoad", Thread.current)
    }
  }
}

И в консоли будет:

viewDidLoad <_NSMainThread: 0x600000300000>{number = 1, name = main}
<NSThread: 0x600000348e80>{number = 5, name = (null)}
<NSThread: 0x600000348e80>{number = 5, name = (null)}
<NSThread: 0x600000306000>{number = 6, name = (null)}
<NSThread: 0x600000340b00>{number = 4, name = (null)}
<NSThread: 0x600000348e80>{number = 5, name = (null)}
viewDidLoad <_NSMainThread: 0x600000300000>{number = 1, name = main}
  1. Таска унаследовала MainActor, поэтому тут видим main поток

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

  3. Тут сразу видим, что первый принт показывает уже не main. Функция уже изначально выполняется на другом потоке. За 5 итераций цикл успел побыть на 3 разных потоках

  4. После завершения SomeClass().someFunc(), оставшаяся часть таски из viewDidLoad планируется и выполняется на MainActor экзекьюторе, и в консоли видим main поток


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

Это учтено неявно. UIViewController, от которого мы наследуемся, помечен атрибутом MainActor. Следовательно, и наш класс наследует этот атрибут. Unstructured Task при создании наследует Actor, поэтому все UI изменения внутри него в нашем случае безопасны.

Привык называть GCD'шные очереди синхронными и асинхронными, хотя правильней говорить последовательные и конкуррентные (перевод concurrent как "параллельная" смущает, тк это тоже иной термин)

Поправил этот момент, спасибо за замечание

Добавлю еще один пункт к списку:

  • Если мы работаем с большим количеством потокоблокирующих тасок, то GCD будет отрабатывать такие случаи быстрее чем async/await. Например, если в async варианте функции из примера про Thread Explosion заменить Task.sleep на просто блокирующий sleep, то GCD вариант выполнится быстрее. Это происходит из-за того что в Cooperative Thread Pull выделяется ограниченное кол-во потоков, и при блокировке они быстро заканчиваются.

Поправил в некоторых моментах термины "многопоточность" на "асинхронность". Действительно это наверное могло запутать читателей.

Спасибо за замечание

Асинхронность и многопоточность в swift - разные понятия. Говоря про async/await - то функции выполняются асинхронно. Но в это же время асинхронная функция может быть выполнена как в одном потоке, так и многопоточно (в зависимости от реализации и контекста)

Information

Rating
Does not participate
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Date of birth
Registered
Activity

Specialization

Mobile Application Developer
Senior
SWIFT
SwiftUI
RxSwift
MVVM
UIKit
iOS development
Xcode
Autolayout
Foundation
GCD