Pull to refresh

Comments 5

Кроме того, он может продолжить выполнение вообще на другом потоке. Об этом важно помнить при работе с UI.

Ни увидел в вашем коде где бы это было учтено.

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

Не совсем ясен весь механизм, поясните пожалуйста. Когда из viewDidLoad вызвана async функция через Task как у вас, то блок таска {...} будет выполнен на MainActor, потому что UIViewController имеет атрибут MainActor как вы сказали. То есть запуск кода загрузки картинки будет на main потоке, но так как под капотом используется URLSession то она перебросит сетевую таску на фоновый поток и после выполнения запроса вернет результат на главный поток ?

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

При работе с 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 поток


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

С логами всё стало понятно. Спасибо большое за проделанный труд!

Sign up to leave a comment.

Articles