web-dev-qa-db-fra.com

Cats-effect et asynchrone IO spécificités

Depuis quelques jours, j'enroule ma tête autour de l'effet chats et de l'OI. Et je sens que j'ai des idées fausses sur cet effet ou simplement j'ai raté son propos.

  1. Tout d'abord - si IO peut remplacer Scala's Future, comment pouvons-nous créer une tâche asynchrone IO tâche? Utiliser IO.shift? Utiliser IO.async? IO.delay Est-il sync ou asynchrone? Pouvons-nous faire une tâche asynchrone générique avec du code comme celui-ci Async[F].delay(...)? Ou async se produit lorsque nous appelons IO avec unsafeToAsync ou unsafeToFuture?
  2. Quel est l'intérêt d'Async et de Concurrent dans l'effet chats? Pourquoi sont-ils séparés?
  3. IO un fil vert? Si oui, pourquoi un objet Fibre dans l'effet chats .

J'apprécierais quelques éclaircissements sur tout cela, car j'ai échoué à comprendre les documents sur les chats sur ceux-ci et Internet n'était pas très utile ...

15
ukulele

si IO peut remplacer Scala's Future, comment pouvons-nous créer une tâche asynchrone IO

Tout d'abord, nous devons clarifier ce que l'on entend par tâche asynchrone . Habituellement async signifie "ne bloque pas le thread OS", mais puisque vous mentionnez Future, c'est un peu flou. Dis, si j'écris:

Future { (1 to 1000000).foreach(println) }

ce ne serait pas asynchrone , car c'est une boucle de blocage et une sortie de blocage, mais il s'exécuterait potentiellement sur un thread OS différent, comme géré par un ExecutionContext implicite . Le code d'effet chats équivalent serait:

for {
  _ <- IO.shift
  _ <- IO.delay { (1 to 1000000).foreach(println) }
} yield ()

(ce n'est pas la version courte)

Alors,

  • IO.shift Est utilisé pour peut-être changer le thread/pool de threads. Future le fait à chaque opération, mais ce n'est pas gratuit en termes de performances.
  • IO.delay {...} (alias IO { ... }) Ne [~ # ~] pas [~ # ~] faire quoi que ce soit asynchrone et [~ # ~] pas [~ # ~] changer de threads. Il est utilisé pour créer des valeurs simples IO à partir d'API synchrones à effets secondaires

Maintenant, revenons à asynchrone vraie . La chose à comprendre ici est la suivante:

Chaque calcul asynchrone peut être représenté comme une fonction prenant le rappel.

Que vous utilisiez une API qui renvoie Future ou Java CompletableFuture, ou quelque chose comme NIO CompletionHandler, tout peut être converti en rappels. Voici à quoi sert IO.async: Vous pouvez convertir n'importe quelle fonction prenant le rappel en IO. Et au cas où:

for {
  _ <- IO.async { ... }
  _ <- IO(println("Done"))
} yield ()

Done ne sera imprimé que lorsque (et si) le calcul dans ... Rappelle. Vous pouvez le considérer comme bloquant le fil vert, mais pas le fil OS.

Alors,

  • IO.async Sert à convertir tout calcul déjà asynchrone en IO.
  • IO.delay Sert à convertir tout calcul complètement synchrone en IO.
  • Le code avec des calculs vraiment asynchrones se comporte comme s'il bloquait un fil vert.

L'analogie la plus proche lorsque vous travaillez avec Future s est de créer un scala.concurrent.Promise Et de renvoyer p.future.


Ou async se produit lorsque nous appelons IO avec unsafeToAsync ou unsafeToFuture?

Sorte de. Avec IO, rien ne se produit sauf si vous appelez l'un d'eux (ou utilisez IOApp). Mais IO ne garantit pas que vous exécuteriez sur un thread OS différent ou même de manière asynchrone, sauf si vous l'avez demandé explicitement avec IO.shift Ou IO.async.

Vous pouvez garantir le changement de thread à tout moment avec par exemple (IO.shift *> myIO).unsafeRunAsyncAndForget(). Cela est possible précisément parce que myIO ne sera pas exécuté tant que cela ne vous sera pas demandé, que vous l'ayez sous la forme val myIO Ou def myIO.

Cependant, vous ne pouvez pas transformer par magie des opérations de blocage en non-blocage. Ce n'est possible ni avec Future ni avec IO.


Quel est l'intérêt d'Async et de Concurrent dans l'effet chats? Pourquoi sont-ils séparés?

Async et Concurrent (et Sync) sont des classes de type. Ils sont conçus pour que les programmeurs puissent éviter d'être verrouillés sur cats.effect.IO Et peuvent vous fournir une API qui prend en charge tout ce que vous choisissez à la place, comme monix Task ou Scalaz 8 ZIO, ou même un type de transformateur monade tel que OptionT[Task, *something*]. Des bibliothèques telles que fs2, monix et http4s les utilisent pour vous donner plus de choix avec quoi les utiliser.

Concurrent ajoute des éléments supplémentaires en plus de Async, les plus importants étant .cancelable Et .start. Ceux-ci n'ont pas d'analogie directe avec Future, car cela ne prend pas du tout en charge l'annulation.

.cancelable Est une version de .async Qui vous permet également de spécifier une logique pour annuler l'opération que vous encapsulez. Un exemple courant est les demandes de réseau - si vous n'êtes plus intéressé par les résultats, vous pouvez simplement les abandonner sans attendre la réponse du serveur et ne pas perdre de sockets ou de temps de traitement en lisant la réponse. Vous pourriez ne jamais l'utiliser directement, mais il a sa place.

Mais à quoi servent les opérations annulables si vous ne pouvez pas les annuler? L'observation clé ici est que vous ne pouvez pas annuler une opération de l'intérieur. Quelqu'un d'autre doit prendre cette décision, et cela se produirait simultanément avec l'opération elle-même (c'est là que la classe de type tire son nom). C'est là qu'intervient .start. En bref,

.start Est un fork explicite d'un fil vert.

Faire someIO.start Revient à faire val t = new Thread(someRunnable); t.start(), sauf qu'il est vert maintenant. Et Fiber est essentiellement une version allégée de l'API Thread: vous pouvez faire .join, Qui est comme Thread#join(), mais il ne bloque pas le thread OS ; et .cancel, qui est une version sûre de .interrupt().


Notez qu'il existe d'autres façons de bifurquer des fils verts. Par exemple, faire des opérations parallèles:

val ids: List[Int] = List.range(1, 1000)
def processId(id: Int): IO[Unit] = ???
val processAll: IO[Unit] = ids.parTraverse_(processId)

fourchera le traitement de tous les ID aux threads verts, puis les joindra tous. Ou en utilisant .race:

val fetchFromS3: IO[String] = ???
val fetchFromOtherNode: IO[String] = ???

val fetchWhateverIsFaster = IO.race(fetchFromS3, fetchFromOtherNode).map(_.merge)

exécutera les récupérations en parallèle, vous donnera le premier résultat terminé et annulera automatiquement la récupération la plus lente. Donc, faire .start Et utiliser Fiber n'est pas le seul moyen de bifurquer plus de fils verts, juste le plus explicite. Et cela répond:

IO un fil vert? Si oui, pourquoi un objet Fibre dans l'effet chats .

  • IO est comme un thread vert, ce qui signifie que vous pouvez en exécuter plusieurs en parallèle sans surcharge de threads du système d'exploitation, et le code de compréhension se comporte comme s'il bloquait le résultat à calculer.

  • Fiber est un outil pour contrôler les threads verts explicitement bifurqués (en attente d'achèvement ou d'annulation).

29
Oleg Pyzhcov