Блокировка потоков в Хаскеле
Я начинаю кодировать 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
Функция запускается в отдельном зеленом потоке для каждого запроса, который может выполнять длительные чисто вычислительные вычисления с привязкой к ЦП, блокировать операции ввода-вывода и все остальное, что требуется, и потоку обработки не нужно передавать результат обратно в основной поток в чтобы окончательно обслужить запрос. Он может просто сообщить результат клиенту напрямую.
Скотти в основном построен на основе этого шаблона, поэтому он автоматически отправляет несколько запросов, не требуя обратных вызовов или блокировки потоков ОС.