если IO может заменить Scala's Future, как мы можем создать задачу асинхронного ввода-вывода
Сначала нам нужно уточнить, что подразумевается под асинхронной задачей .Обычно async означает «не блокирует поток ОС», но, поскольку вы упоминаете Future
, это немного размыто.Скажем, если бы я написал:
Future { (1 to 1000000).foreach(println) }
, это не было бы async , так как это блокирующий цикл и блокирующий вывод, но он мог бы потенциально выполняться в другом потоке ОС, управляемомнеявный ExecutionContext.Эквивалентный код эффекта кошки будет:
for {
_ <- IO.shift
_ <- IO.delay { (1 to 1000000).foreach(println) }
} yield ()
(это не укороченная версия)
Итак,
IO.shift
используется для возможного измененияпоток / пул потоков.Future
делает это при каждой операции, но не с точки зрения производительности. IO.delay
{...} (он же IO { ... }
) NOT делает что-то асинхронным и делает НЕ переключение потоков.Он используется для создания простых IO
значений из синхронных API с побочными эффектами
Теперь вернемся к true async .Здесь нужно понять следующее:
Каждое асинхронное вычисление может быть представлено как функция с обратным вызовом.
Используете ли вы API, который возвращает Future
илиJava CompletableFuture
или что-то вроде NIO CompletionHandler
, все это можно преобразовать в обратные вызовы.Вот для чего IO.async
: вы можете преобразовать любую функцию с обратным вызовом в IO
.И в случае, как:
for {
_ <- IO.async { ... }
_ <- IO(println("Done"))
} yield ()
Done
будет напечатан только тогда, когда (и если) вычисления в ...
обратного вызова.Вы можете думать об этом как о блокировке зеленого потока, но не потока ОС.
Итак,
IO.async
предназначен для преобразования любых уже асинхронных вычислений вIO
. IO.delay
предназначен для преобразования любых полностью синхронных вычислений в IO
. - Код с действительно асинхронными вычислениями ведет себя так, как будто он блокирует зеленый поток.
Самая близкая аналогия при работе с Future
s - это создание scala.concurrent.Promise
и возвращение p.future
.
Или асинхронность происходит, когда мы вызываемIO с unsafeToAsync или unsafeToFuture?
В некотором роде.С IO
, ничего не произойдет, если вы не позвоните одному из них (или не используете IOApp
).Но IO не гарантирует, что вы будете работать в другом потоке ОС или даже асинхронно, если только вы не попросите об этом явно с помощью IO.shift
или IO.async
.
. Вы можете гарантировать переключение потоков в любое время, например, (IO.shift *> myIO).unsafeRunAsyncAndForget()
,Это возможно именно потому, что myIO
не будет выполняться до тех пор, пока его не попросят, независимо от того, есть ли у него значение val myIO
или def myIO
.
Однако нельзя магическим образом преобразовывать блокирующие операции в неблокирующие.Это невозможно ни с Future
, ни с IO
.
В чем смысл Async и Concurrent в эффекте кошки?Почему они разделены?
Async
и Concurrent
(и Sync
) являются классами типов.Они разработаны таким образом, что программисты могут избежать блокировки на cats.effect.IO
и могут предоставить вам API, который поддерживает все, что вы выберете вместо этого, например, monix Task или Scalaz 8 ZIO, или даже тип монадного преобразователя, такой как OptionT[Task, *something*]
.Библиотеки, такие как fs2, monix и http4, используют их, чтобы дать вам более широкий выбор того, с чем их использовать.
Concurrent
добавляет дополнительные элементы поверх Async
, наиболее важными из которых являются .cancelable
и .start
.Они не имеют прямой аналогии с Future
, поскольку они вообще не поддерживают отмену.
.cancelable
- это версия .async
, которая позволяет вам также указать некоторую логику для отмены операции, которую выЗаворачиваем.Типичным примером являются сетевые запросы - если вас больше не интересуют результаты, вы можете просто прервать их, не дожидаясь ответа сервера и не тратя впустую сокеты или время обработки на чтение ответа.Вы никогда не могли бы использовать это непосредственно, но у этого есть свое место.
Но что хорошего в отменяемых операциях, если вы не можете их отменить?Ключевое наблюдение здесь заключается в том, что вы не можете отменить операцию изнутри себя.Кто-то другой должен принять это решение, и это произойдет одновременно с самой операцией (где класс типов получает свое имя).Вот где приходит .start
. Короче говоря,
.start
- это явная ветвь зеленой нити.
Выполнение someIO.start
похоже на выполнение val t = new Thread(someRunnable); t.start()
, только теперь он зеленый.И Fiber
по сути является урезанной версией Thread
API: вы можете сделать .join
, что похоже на Thread#join()
, но оно не блокирует поток ОС;и .cancel
, который является безопасной версией .interrupt()
.
Обратите внимание, что существуют и другие способы ветвления зеленых нитей.Например, при выполнении параллельных операций:
val ids: List[Int] = List.range(1, 1000)
def processId(id: Int): IO[Unit] = ???
val processAll: IO[Unit] = ids.parTraverse_(processId)
разветвит обработку всех идентификаторов зелеными потоками, а затем объединит их все.Или, используя .race
:
val fetchFromS3: IO[String] = ???
val fetchFromOtherNode: IO[String] = ???
val fetchWhateverIsFaster = IO.race(fetchFromS3, fetchFromOtherNode).map(_.merge)
, вы будете выполнять выборки параллельно, давать вам первый завершенный результат и автоматически отменять более медленную выборку.Таким образом, выполнение .start
и использование Fiber
- не единственный способ форкировать больше зеленых потоков, просто самый явный.И это отвечает:
Является ли IO зеленой нитью?Если да, то почему в эффекте кошки есть волоконный объект?Как я понимаю, Fiber - это зеленая нить, но документы утверждают, что мы можем думать о IO как о зеленых нитях.
IO
походит на зеленую нить, то есть вы можете иметьмногие из них работают параллельно без издержек на потоки ОС, и код для понимания ведет себя так, как будто он блокирует вычисляемые результаты.
Fiber
- инструментдля управления зелеными нитями в явном виде (ожидание завершения или отмены).