


De nombreuses langues, telles que Kotlin, Go, JavaScript, Python, Rust, C #, C ++ et autres, ont déjà une prise en charge de Coroutines qui rend la mise en œuvre de la motif asynchrones / attendre possible. Cette fonctionnalité n'est pas encore prise en charge dans Swift, mais cela peut être amélioré par un cadre sans avoir besoin de changer la langue.
La programmation asynchrone est généralement associée aux rappels. C'est tout à fait pratique jusqu'à ce qu'il y ait trop et qu'ils commencent à nicher. Ensuite, ça s'appelle une pyramide de malheur ou même de l'enfer de rappel .
Un autre problème de programmation asynchrone est la gestion des erreurs , car le mécanisme de gestion des erreurs naturelles de Swift ne peut pas être utilisé.
Il existe de nombreux autres frameworks qui facilitent l'utilisation de code asynchrone, tels que Combine, Rxswift, Promisekit, etc. Ils utilisent d'autres approches qui présentent certains inconvénients:
Le motif asynchrone / attente est une alternative qui permet de structurer une fonction asynchrone et non bloquante d'une manière similaire à une fonction synchrone ordinaire.
Il est déjà bien établi dans d'autres langages de programmation et est une évolution de la programmation asynchrone. La mise en œuvre de ce modèle est possible grâce aux coroutines.
Jetons un coup d'œil à l'exemple avec Coroutine à l'intérieur de laquelle await() le suspend et reprend lorsque le résultat est disponible sans bloquer le fil.
//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
}Documentation API
Une coroutine est un calcul qui peut être suspendu et reprendre plus tard sans bloquer un fil. Les coroutines s'appuient sur des fonctions régulières et peuvent être exécutées sur n'importe quel planificateur avec la possibilité de basculer parmi eux pendant l'exécution.
La conception de l'API Coroutines est aussi minimaliste que possible. Il se compose du protocole CoroutineScheduler qui décrit comment planifier les coroutines ( DispatchQueue le conforme déjà), et la structure Coroutine avec des méthodes d'utilité. Cette API est suffisante pour faire des choses incroyables.
L'exemple suivant montre l'utilisation d' await() à l'intérieur d'une coroutine pour envelopper les appels asynchrones.
//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 . . .
} Voici comment nous pouvons nous conformer NSManagedObjectContext à CoroutineScheduler pour avoir lancé des coroutines dessus.
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 avenir est un titulaire en lecture seule pour un résultat qui sera fourni plus tard et la promesse est le fournisseur de ce résultat. Ils représentent l'achèvement ou l'échec éventuel d'une opération asynchrone.
L'approche des futurs et des promesses elle-même est devenue un standart de l'industrie. Il s'agit d'un mécanisme pratique pour synchroniser le code asynchrone. Mais avec les coroutines, il prend l'utilisation du code asynchrone au niveau supérieur et est devenu une partie du modèle async / attente. Si les coroutines sont un squelette, les futurs et les promesses sont ses muscles.
Les futurs et les promesses sont représentés par la classe CoFuture correspondante et sa sous-classe CoPromise .
//wraps some async func with CoFuture
func makeIntFuture ( ) -> CoFuture < Int > {
let promise = CoPromise < Int > ( )
someAsyncFunc { int in
promise . success ( int )
}
return promise
} Il permet de démarrer plusieurs tâches en parallèle et de les synchroniser plus tard avec 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 ) "
} Il est très facile de transformer ou de composer CoFuture S en un nouveau.
let array : [ CoFuture < Int > ]
//create new CoFuture<Int> with sum of future results
let sum = CoFuture { try array . reduce ( 0 ) { try $0 + $1 . await ( ) } }Les futures et les promesses fournissent un moyen pratique de transférer une seule valeur entre les coroutines. Les canaux fournissent un moyen de transférer un flux de valeurs. Conceptuellement, un canal est similaire à une file d'attente qui permet de suspendre une coroutine lors de la réception si elle est vide, ou d'envoyer si elle est pleine.
Cette primitive non bloquante est largement utilisée dans des langues telles que Go et Kotlin, et c'est un autre instrument qui améliore le travail avec les coroutines.
Pour créer des canaux, utilisez la classe 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 " )
} Tous les coroutines, CoFuture et CoChannel lancés, ne doivent généralement pas être référencés. Ils sont désintés après leur exécution. Mais souvent, il est nécessaire de les compléter plus tôt, lorsqu'ils ne sont plus nécessaires. Pour cela, CoFuture et CoChannel ont des méthodes d'annulation.
CoScope facilite la gestion du cycle de vie de ces objets. Il vous permet de garder des références faibles à eux et d'annuler si nécessaire ou sur déinit.
Vous pouvez ajouter des coroutines, des CoFuture , CoChannel et d'autres CoCancellable au CoScope pour les annuler lorsqu'ils ne sont plus nécessaires ou sur déinit.
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 . . .
}
}