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.
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
?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 ...
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 secondairesMaintenant, 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
.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).