好的,这里有很多东西要解压。首先,让我们讨论一下您的具体代码示例。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
链接立即返回,即使slow
和pure
链接分别在 IO 和纯计算上被阻塞。(没有什么特别的threadDelay
——它可能是任何 IO 操作,例如访问数据库或读取大文件或代理到另一个 HTTP 服务器或其他任何东西。)您可以继续为 、 和 启动多个附加请求,fast
以及缓慢的请求当服务器继续接受更多请求时,将在后台突然消失。(计算方式与slow
pure
pure
slow
计算——它只会在第一次阻塞,所有等待它的线程将立即返回答案,后续请求会很快。如果我们欺骗 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 线程。