


Muchos idiomas, como Kotlin, GO, JavaScript, Python, Rust, C#, C ++ y otros, ya tienen soporte de coroutinas que hace posible la implementación del patrón de Async/espera. Esta característica aún no es compatible en Swift, pero esto puede mejorarse mediante un marco sin la necesidad de cambiar el idioma.
La programación asincrónica generalmente se asocia con devoluciones de llamada. Es bastante conveniente hasta que hay demasiados y comienzan a anidar. Luego se llama una pirámide de fatalidad o incluso un infierno de devolución de llamada .
Otro problema de la programación asincrónica es el manejo de errores , porque el mecanismo de manejo de errores naturales de Swift no se puede usar.
Hay muchos otros marcos que facilitan el uso de código asíncrono, como Combine, RxSwift, PromiseKit, etc. Utilizan otros enfoques que tienen algunos inconvenientes:
El patrón de async/esperanza es una alternativa que permite que una función asíncrona y sin bloqueo sea estructurada de manera similar a una función sincrónica ordinaria.
Ya está bien establecido en otros lenguajes de programación y es una evolución en la programación asincrónica. La implementación de este patrón es posible gracias a las coroutinas.
Echemos un vistazo al ejemplo con coroutine dentro del cual await() lo suspende y se reanuda cuando el resultado está disponible sin bloquear el hilo.
//executes coroutine on the main thread
DispatchQueue . main . startCoroutine {
//extension that returns CoFuture<(data: Data, response: URLResponse)>
let dataFuture = URLSession . shared . dataTaskFuture ( for : imageURL )
//await CoFuture result that suspends coroutine and doesn't block the thread
let data : Data = try dataFuture . await ( ) . data
//create UIImage from the data
guard let image = UIImage ( data : data ) else { return }
//execute heavy task on global queue and await the result without blocking the thread
let thumbnail : UIImage = try DispatchQueue . global ( ) . await { image . makeThumbnail ( ) }
//set image in UIImageView on the main thread
self . imageView . image = thumbnail
}Documentación de API
Una coroutina es un cálculo que se puede suspender y reanudar en un momento posterior sin bloquear un hilo. Las coroutinas se basan en funciones regulares y se pueden ejecutar en cualquier programador con la posibilidad de cambiar entre ellas durante la ejecución.
El diseño de la API de Coroutinas es lo más minimalista posible. Consiste en el protocolo CoroutineScheduler que describe cómo programar coroutinas ( DispatchQueue ya lo conforma) y la estructura Coroutine con métodos de utilidad. Esta API es suficiente para hacer cosas increíbles.
El siguiente ejemplo muestra el uso de await() dentro de una coroutina para envolver llamadas asíncronas.
//execute coroutine on the main thread
DispatchQueue . main . startCoroutine {
//await URLSessionDataTask response without blocking the thread
let ( data , response , error ) = try Coroutine . await { callback in
URLSession . shared . dataTask ( with : url , completionHandler : callback ) . resume ( )
}
. . . use response on the main thread . . .
} Así es como podemos conformarnos NSManagedObjectContext a CoroutineScheduler para lanzarle coroutinas.
extension NSManagedObjectContext : CoroutineScheduler {
func scheduleTask ( _ task : @escaping ( ) -> Void ) {
perform ( task )
}
}
//execute coroutine on the main thread
DispatchQueue . main . startCoroutine {
let context : NSManagedObjectContext //context with privateQueueConcurrencyType
let request : NSFetchRequest < NSDictionary > //some complex request
//execute request on the context without blocking the main thread
let result : [ NSDictionary ] = try context . await { try context . fetch ( request ) }
}Un futuro es un titular de solo lectura para un resultado que se proporcionará más adelante y la promesa es el proveedor de este resultado. Representan la finalización eventual o la falla de una operación asincrónica.
El enfoque de futuros y promesas en sí se ha convertido en un estándar de la industria. Es un mecanismo conveniente para sincronizar el código asincrónico. Pero junto con las coroutinas, lleva el uso del código asíncrono al siguiente nivel y se ha convertido en parte del patrón de Async/espera. Si las coroutinas son un esqueleto, entonces los futuros y las promesas son sus músculos.
Los futuros y las promesas están representadas por la clase CoFuture correspondiente y su subclase CoPromise .
//wraps some async func with CoFuture
func makeIntFuture ( ) -> CoFuture < Int > {
let promise = CoPromise < Int > ( )
someAsyncFunc { int in
promise . success ( int )
}
return promise
} Permite iniciar múltiples tareas en paralelo y sincronizarlas más tarde con await() .
//create CoFuture<Int> that takes 2 sec. from the example above
let future1 : CoFuture < Int > = makeIntFuture ( )
//execute coroutine on the global queue and returns CoFuture<Int> with future result
let future2 : CoFuture < Int > = DispatchQueue . global ( ) . coroutineFuture {
try Coroutine . delay ( . seconds ( 3 ) ) //some work that takes 3 sec.
return 6
}
//execute coroutine on the main thread
DispatchQueue . main . startCoroutine {
let sum : Int = try future1 . await ( ) + future2 . await ( ) //will await for 3 sec.
self . label . text = " Sum is ( sum ) "
} Es muy fácil transformar o componer CoFuture s en uno nuevo.
let array : [ CoFuture < Int > ]
//create new CoFuture<Int> with sum of future results
let sum = CoFuture { try array . reduce ( 0 ) { try $0 + $1 . await ( ) } }Los futuros y las promesas proporcionan una forma conveniente de transferir un valor único entre las corutinas. Los canales proporcionan una forma de transferir un flujo de valores. Conceptualmente, un canal es similar a una cola que permite suspender una coroutina al recibir si está vacía o enviar si está lleno.
Este primitivo no bloqueado se usa ampliamente en idiomas como Go y Kotlin, y es otro instrumento que mejora el trabajo con las coroutinas.
Para crear canales, use la clase CoChannel .
//create a channel with a buffer which can store only one element
let channel = CoChannel < Int > ( capacity : 1 )
DispatchQueue . global ( ) . startCoroutine {
for i in 0 ..< 100 {
//imitate some work
try Coroutine . delay ( . seconds ( 1 ) )
//sends a value to the channel and suspends coroutine if its buffer is full
try channel . awaitSend ( i )
}
//close channel when all values are sent
channel . close ( )
}
DispatchQueue . global ( ) . startCoroutine {
//receives values until closed and suspends a coroutine if it's empty
for i in channel . makeIterator ( ) {
print ( " Receive " , i )
}
print ( " Done " )
} Todas las coroutinas lanzadas, CoFuture s y CoChannel s, generalmente no necesitan ser referenciadas. Se desiniten después de su ejecución. Pero a menudo es necesario completarlos antes, cuando ya no son necesarios. Para esto, CoFuture y CoChannel tienen métodos para cancelar.
CoScope facilita la gestión del ciclo de vida de estos objetos. Le permite mantener referencias débiles a ellos y cancelar si es necesario o en Deinit.
Puede agregar coroutinas, CoFuture s, CoChannel y otros CoCancellable a CoScope para cancelarlas cuando ya no son necesarios o en Deinit.
class ViewController : UIViewController {
let scope = CoScope ( ) //will cancel all objects on `cancel()` or deinit
func performSomeWork ( ) {
//create new `CoChannel` and add to `CoScope`
let channel = makeSomeChannel ( ) . added ( to : scope )
//execute coroutine and add to `CoScope`
DispatchQueue . main . startCoroutine ( in : scope ) { [ weak self ] in
for item in channel . makeIterator ( ) {
try self ? . performSomeWork ( with : item )
}
}
}
func performSomeWork ( with item : Item ) throws {
//create new `CoFuture` and add to `CoScope`
let future = makeSomeFuture ( item ) . added ( to : scope )
let result = try future . await ( )
. . . do some work using result . . .
}
}