Особенности кошачьего эффекта и асинхронного ввода-вывода - PullRequest
0 голосов
/ 08 декабря 2018

В течение нескольких дней я оборачивался вокруг эффекта кошки и IO.И я чувствую, что у меня есть некоторые неправильные представления об этом эффекте или я просто упустил его.

  1. Прежде всего - если IO может заменить Scala's Future, как мы можем создать асинхронную задачу IO?Используете IO.shift?Используете IO.async?IO.delay синхронизируется или асинхронен?Можем ли мы сделать общую асинхронную задачу с кодом, подобным этому Async[F].delay(...)?Или асинхронность происходит, когда мы вызываем IO с unsafeToAsync или unsafeToFuture?
  2. Какой смысл асинхронности и одновременности в эффекте кошки?Почему они разделены?
  3. Является ли IO зеленой нитью?Если да, то почему в эффекте кошки есть волоконный объект?Как я понимаю, Fiber - это зеленая нить, но в документации утверждают, что мы можем думать о IO как о зеленых нитях.

Я был бы признателен за некоторые разъяснения по всем этим вопросам, так как я не смог понять документы о влиянии кошек нате и интернет не были так полезны ...

1 Ответ

0 голосов
/ 10 декабря 2018

если 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 и ConcurrentSync) являются классами типов.Они разработаны таким образом, что программисты могут избежать блокировки на 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 - инструментдля управления зелеными нитями в явном виде (ожидание завершения или отмены).

...