1

我开始使用 Haskell 进行异步编码,现在我正在使用forkIO它创建一个绿色线程(对吗?是绿色线程吗?),然后我使用 aMVar从新线程与主线程通信一次我完成了,我有价值。这是我的代码:

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可以创建一个 ThreadB并接收一个Future.

然后订阅一个回调函数,一旦线程完成该值onComplete,它将被调用。B

但是在那段时间里,线程A并没有被阻塞,并且可以被重用于其他操作。

例如,在我们的 Http 服务器框架中VertxGrizzly通常配置为具有少量操作系统线程(4-8),因为它们永远不应该被阻塞。

难道我们在 Haskell 中没有另一种纯粹的无阻塞机制吗?

问候

4

1 回答 1

5

好的,这里有很多东西要解压。首先,让我们讨论一下您的具体代码示例。responseUsers为 Scotty编写处理程序的正确方法是:

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链接立即返回,即使slowpure链接分别在 IO 和纯计算上被阻塞。(没有什么特别的threadDelay——它可能是任何 IO 操作,例如访问数据库或读取大文件或代理到另一个 HTTP 服务器或其他任何东西。)您可以继续为 、 和 启动多个附加请求,fast以及缓慢的请求当服务器继续接受更多请求时,将在后台突然消失。(计算方式与slowpurepureslow计算——它只会在第一次阻塞,所有等待它的线程将立即返回答案,后续请求会很快。如果我们欺骗 Haskell 为每个请求重新计算它,或者如果它实际上依赖于请求中提供的某些信息,就像在更现实的服务器中可能出现的情况一样,它的行为或多或少类似于slow计算。)

您在这里不需要任何类型的回调,也不需要主线程“等待”结果。Scotty 为处理每个请求而分叉的线程可以执行所需的任何计算或 IO 活动,然后直接将响应返回给客户端,而不会影响任何其他线程。

更重要的是,除非您编译此服务器-threaded并在编译或运行时提供 >1 的线程数,否则它只能在一个 OS 线程中运行。 因此,默认情况下,它会自动在单个操作系统线程中完成所有这些工作!

其次,这对斯科蒂来说实际上并没有什么特别之处。您应该将 Haskell 运行时视为在 OS 线程机制之上提供线程抽象层,并且 OS 线程是您不必担心的实现细节(好吧,除非在不寻常的情况下,例如如果您重新与需要在某些操作系统线程中发生某些事情的外部库进行交互)。

因此,所有 Haskell 线程,甚至是“主”线程,都是绿色的,并且运行在一种虚拟机之上,该虚拟机将在单个 OS 线程之上正常运行,无论有多少绿色线程因任何原因阻塞.

因此,编写异步请求处理程序的典型模式是:

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

请注意,这里不需要回调。该handleRequest函数针对每个请求在单独的绿色线程中运行,该线程可以执行长时间运行的纯 CPU 绑定计算、阻塞 IO 操作以及其他任何需要的操作,并且处理线程不需要将结果传回主线程为了最终服务于请求。它可以直接将结果传达给客户端。

Scotty 基本上是围绕这种模式构建的,因此它会自动分派多个请求,而无需回调或阻塞 OS 线程。

于 2018-08-18T17:26:47.583 回答