Swift async-attesa vs chiusure

Dec 07 2022
Swift wait funziona acquisendo il contesto e sospendendo l'esecuzione fino al ritorno del metodo asincrono chiamato. Un'altra cosa che funziona in modo simile catturando il contesto circostante è una chiusura sfuggente.

Swift awaitfunziona catturando il contesto e sospendendo l'esecuzione fino al asyncritorno del metodo chiamato. Un'altra cosa che funziona in modo simile catturando il contesto circostante è una chiusura sfuggente. Quindi async-awaitle chiamate possono essere immaginate come equivalenti a sfuggire alla chiusura. Ogni volta che vedi un metodo con asyncsostituisci mentalmente quel metodo con un gestore di completamento di escape.

func run() async
func run(_ completion: @escaping () -> Void)

Uno scenario semplice con codice equivalente

class Foo {
  func before() { /*...*/ }
  func after() { /*...*/ }

  func doStuff() async { /*...*/ }
  func doStuff(_ completion: @escaping () -> Void) { /*...*/ }
}
extension Foo {
  func run() async {
    before()
    await doStuff()
    after()
  }
}
extension Foo {
  func run(_ completion: @escaping () -> Void) {
    before()
    doStuff {
      self.after()
      completion()
    }
  }
}

func doStuff() async -> Int

let ret = await doStuff()
print("\(ret)")
func doStuff(_ completion: @escaping (Int) -> Void)

doStuff { ret in
    print("\(ret)")
}

func div(_ a: Int, _ b: Int) async throws -> Double {
  if b == 0 {
    throw FooError.divideByZero
  }
  return Double(a) / Double(b)
}

do {
  let ret = try await div(1, 0)
  print("\(ret)")
} catch {
  print("\(error)")
}
func div(_ a: Int, _ b: Int, _ completion: @escaping (Double) -> Void) throws {
  if b == 0 {
    throw FooError.divideByZero
  }
  completion(Double(a) / Double(b))
}

do {
  try div(1, 0) { ret in
    print("\(ret)")
  }
} catch {
  print("\(error)")
}

Nota come la asyncchiamata si propaga naturalmente al chiamato. Quindi anche una chiamata a doStuff() asyncfa run() async. Nella maggior parte dei casi, questo ha senso anche per coloro che effettuano il completamento. E se desideriamo non propagare il comportamento asincrono, o in altre parole, desideriamo convertire un asyncmetodo in un syncmetodo, dobbiamo usare a Taskche accetta un gestore di completamento ed è equivalente al wrapping all'internoDispatchQueue.async { ... }

func run() {
  before()
  Task {
    await doStuff()
    after()
  }
}
func run() {
  before()
  DispatchQueue.global().async {
    self.doStuff {
      self.after()
    }
  }
}

Se desideriamo chiamare i metodi asincroni uno dopo l'altro, il modello mentale rimane lo stesso

func doStuff() async {}
func doMoreStuff() async {}

before()
await doStuff()
await doMoreStuff()
after()
func doStuff(_ completion: @escaping () -> Void) {}
func doMoreStuff(_ completion: @escaping () -> Void) {}

before()
doStuff {
  self.doMoreStuff {
    self.after()
  }
}

func run() async {
  before()
  async let task1: Void = doStuff()
  async let task2: Void = doMoreStuff()
  _ = await [task1, task2]
  after()
}

func run(_ completion: @escaping () -> Void) {
  before()
  let group = DispatchGroup()
  group.enter()
  doStuff { group.leave() }
  group.enter()
  doMoreStuff { group.leave() }
  group.notify(queue: DispatchQueue.global()) {
    self.after()
    completion()
  }
}

Avere un modello mentale migliore per l'attesa asincrona aiuta ad apprezzare il tipo di insidie ​​​​da cui ci salva e anche come migrare dai gestori di completamento all'attesa asincrona.

Ulteriori letture