Блокировка потоков в Хаскеле

Я начинаю кодировать Async с Haskell, и сейчас я использую forkIO которые создают зеленую нить (это правильно? зеленая нить?), а затем я использую MVar связаться с новым потоком с основным потоком, как только я закончу, и у меня будет значение. Вот мой код:

responseUsers :: ActionM ()
responseUsers = do emptyVar <- liftAndCatchIO $newEmptyMVar
                   liftAndCatchIO $ forkIO $ do
                                             users <- getAllUsers
                                             putMVar emptyVar users
                   users <- liftAndCatchIO $ takeMVar emptyVar
                   json (show users) 

После прочтения MVar класс, который я вижу, является классом блочного потока, где, если MVar пустой блок, поток будет заполнен.

Я прихожу из Scala где в другом случае избегать блока у нас есть концепция обратных вызовов в объекте Future, где поток A может создать тему B и получить Future,

Тогда подпишитесь на функцию обратного вызова onComplete который будет вызван как только поток B закончил со значением.

Но за это время нить A не блокируется и может быть повторно использован для других операций.

Например, в нашей среде сервера Http, как Vertx или же Grizzly обычно настраивается на небольшое количество потоков ОС (4-8), поскольку они никогда не должны блокироваться.

Разве у нас нет другого чистого безблокировочного механизма в Haskell?

С уважением

1 ответ

Решение

Хорошо, здесь есть что распаковать. Во-первых, давайте обсудим ваш конкретный пример кода. Правильный способ написать свой responseUsers Обработчик для Скотти это:

responseUsers :: ActionM ()
responseUsers = do
  users <- getAllUsers
  json (show users)

Даже если getAllUsers требуется полтора дня для запуска, и сотня клиентов все делают getAllUsers запросы сразу, больше ничего не будет блокироваться, и ваш сервер Scotty продолжит обрабатывать запросы. Чтобы увидеть это, рассмотрим следующий сервер:

{-# LANGUAGE OverloadedStrings #-}

import Web.Scotty
import Control.Concurrent
import Control.Monad.IO.Class
import qualified Data.Text.Lazy as T

main = scotty 8080 $ do
  get "/fast" $ html "<h1>Fast Response</h1><p>I'm ready!"
  get "/slow" $ liftIO (threadDelay 30000000) >> html "<h1>Slow</h1><p>Whew, finally!"
  get "/pure" $ html $ "<h1>Answer</h1><p>The answer is " 
                <> (T.pack . show . sum $ [1..1000000000])

Если вы скомпилируете это и запустите, вы можете открыть несколько вкладок браузера, чтобы:

http://localhost:8080/slow
http://localhost:8080/pure
http://localhost:8080/fast

и вы увидите, что fast ссылка возвращается немедленно, даже если slow а также pure ссылки заблокированы на IO и чистых вычислениях соответственно. (Там нет ничего особенного threadDelay - это могло быть любое действие ввода-вывода, например, доступ к базе данных, чтение большого файла, прокси на другой HTTP-сервер и т. д.) Вы можете продолжать запускать несколько дополнительных запросов для fast, slow, а также pureи медленные переходят в фоновый режим, в то время как сервер продолжает принимать больше запросов. (The pure вычисление немного отличается от slow вычисление - оно блокируется только в первый раз, все ожидающие его потоки возвращают ответ сразу, и последующие запросы будут быстрыми. Если бы мы обманули Haskell в пересчете его для каждого запроса, или если он действительно зависел от некоторой информации, предоставленной в запросе, как это может быть в случае более реалистичного сервера, он бы действовал более или менее подобно slow вычисления, хотя.)

Здесь вам не нужен какой-либо обратный вызов, и вам не нужен основной поток для "ожидания" результата. Потоки, которые Скотти разветвляет для обработки каждого запроса, могут выполнять любые необходимые вычисления или операции ввода-вывода, а затем возвращать ответ клиенту напрямую, не затрагивая другие потоки.

Более того, если вы не скомпилировали этот сервер с -threaded и предоставить число потоков>1 во время компиляции или во время выполнения, оно работает только в одном потоке ОС. Таким образом, по умолчанию, он делает все это в одном потоке ОС автоматически!

Во-вторых, в этом нет ничего особенного в Скотти. Вы должны думать о среде выполнения Haskell как о предоставлении многопоточного уровня абстракции поверх механизма потоков ОС, а потоки ОС - это деталь реализации, о которой вам не нужно беспокоиться (ну, за исключением необычных ситуаций, например, если вы взаимодействие с внешней библиотекой, которая требует определенных событий в определенных потоках ОС).

Таким образом, все потоки Haskell, даже "основной" поток, являются зелеными и работают поверх виртуальной машины, которая прекрасно работает поверх одного потока ОС, независимо от того, сколько зеленых потоков блокирует по какой-либо причине.,

Следовательно, типичный шаблон для написания асинхронного обработчика запросов:

loop :: IO ()
loop = do
  req <- getRequest
  forkIO $ handleRequest req
  loop

Обратите внимание, что здесь нет необходимости в обратном вызове. handleRequest Функция запускается в отдельном зеленом потоке для каждого запроса, который может выполнять длительные чисто вычислительные вычисления с привязкой к ЦП, блокировать операции ввода-вывода и все остальное, что требуется, и потоку обработки не нужно передавать результат обратно в основной поток в чтобы окончательно обслужить запрос. Он может просто сообщить результат клиенту напрямую.

Скотти в основном построен на основе этого шаблона, поэтому он автоматически отправляет несколько запросов, не требуя обратных вызовов или блокировки потоков ОС.

Другие вопросы по тегам