Блокировка потоков в Haskell

Я начинаю кодировать асинхронный код с помощью 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, где в другом блоке do escape у нас есть концепция обратных вызовов в объекте Future, где поток A может создать поток B и получить Future.

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

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

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

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

С Уважением


person paul    schedule 18.08.2018    source источник
comment
Потоки Haskell очень дешевы, поскольку среда выполнения может запускать многие из них в одном потоке ОС. Это действительно зеленые нити. Часто блокирование потоков является самым простым и наиболее эффективным выбором - среда выполнения в любом случае будет повторно использовать поток ОС для других зеленых потоков, поэтому ничего не теряется. Синхронизируйте зеленые потоки, используя MVars или TVars (или любой другой примитив параллелизма), как вы делали выше, и не бойтесь вызывать forkIO много раз (даже для коротких задач), если вам нужно больше параллелизма.   -  person chi    schedule 18.08.2018
comment
Итак, вы сказали, что, например, в моей реализации Http-сервера, когда я получаю запрос, он работает не в ОС, а в зеленом потоке, поэтому программа может продолжать получать запрос? если это так, делает это автоматически.   -  person paul    schedule 18.08.2018
comment
Потому что здесь, в моем примере, поток ОС - это тот, который создает зеленый поток и блокируется до тех пор, пока зеленый поток не закончится.   -  person paul    schedule 18.08.2018
comment
В приведенном выше коде слишком много синхронизации. Создается один новый поток, и исходный поток немедленно ожидает его завершения (takeMVar). Это и так бессмысленно. Вместо этого это может быть полезно, если исходный поток что-то сделал после forkIO и перед takeMVar.   -  person chi    schedule 18.08.2018
comment
Нет, идея заключалась в том, чтобы освободить поток ОС, чтобы он мог обрабатывать новый запрос, и оставить зеленый поток для сохранения / чтения базы данных.   -  person paul    schedule 18.08.2018
comment
Я не знаком со Скотти, но, возможно, это уже запустило для вас ветку Haskell. Если это так, вы можете написать синхронный код в своем обработчике, поскольку он уже выполняется в своем собственном потоке. Чтобы проверить эту гипотезу, добавьте несколько отпечатков задержки / отладки и получите доступ к вашему серверу из двух окон одновременно - они должны обрабатываться параллельно.   -  person chi    schedule 18.08.2018
comment
Хорошо, я попробую. Теперь я хочу перефразировать свой вопрос. Как Haskell из потока ОС может создать зеленый поток и не блокироваться в ожидании завершения этого потока.   -  person paul    schedule 18.08.2018
comment
RTS на самом деле не выполняет блокирующие системные вызовы - он использует что-то вроде select / poll в системах POSIX для ожидания ввода-вывода и отправки его ожидающим потокам. Кроме того, он выполняет переключение контекста между зелеными потоками каждые N мс или около того.   -  person chi    schedule 18.08.2018


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

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

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

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

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

Поэтому типичный шаблон для написания обработчика асинхронных запросов:

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

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

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

person K. A. Buhr    schedule 18.08.2018
comment
Большое спасибо за такое прекрасное объяснение того, как Haskell заботится о потоках ОС, а мы запускаем только зеленые потоки !. Что касается использования ForkIO в Скотти, это просто потому, что я изучаю асинхронное программирование. - person paul; 18.08.2018