请注意,forkIO 生成绿色线程,而不是 O/S 线程,因此如果生成 1000 个 forkIO 线程来处理同时请求,则不对应于 1000 个单独的 O/S(或“CPU”)线程。
无论如何,是的,RTS 可以同时处理多个阻塞读/写调用,而不会占用 O/S 线程。事实上,默认情况下,当你编译和运行一个没有-threaded 标志或没有-N RTS 选项的 Haskell 程序时,它会在一个单个 O/S 线程中运行所有内容,甚至是你“ fork" 与forkIO,并且多个绿色线程仍然可以阻塞而不阻塞(唯一的)O / S线程。
您也永远不会在 1000 个独立的 O/S 线程上运行 Haskell 服务器。 RTS 可以在一个小的 O/S 线程池上调度数千个绿色线程,比在其自己的 O/S 线程上运行每个绿色线程更有效,因为在绿色线程之间切换不需要昂贵的 O/S 上下文切换。
您可能会发现this 2012 article about the Warp web server library 很有帮助。特别是,他们比较和对比了多种可能的架构:每个请求一个 CPU 线程(即您想象的设计)、事件驱动架构、每个内核一个事件处理进程的混合架构,以及轻量级的 Haskell 模型在 O/S 线程池上运行的线程。请注意,Warp 在后台使用Network.Socket。在他们的基准测试设置中,他们使用 1-10 个 O/S 线程同时处理 1000 个客户端的请求。
如果你想要一些具体的证据证明forkIO 线程不会阻塞,这里有一个玩具程序:
import Control.Monad
import Control.Concurrent
import Network.Socket hiding (recv)
import Network.Socket.ByteString
import qualified Data.ByteString.Char8 as BS
forks = 10
main = withSocketsDo $ do
s <- socket AF_INET Datagram defaultProtocol
bind s (SockAddrInet 6667 (tupleToHostAddress (127,0,0,1)))
replicateM_ forks $ forkIO (BS.putStrLn =<< recv s 4096)
let loop = (putStrLn =<< getLine) >> loop
loop
如果你在没有线程的情况下编译并运行它:
$ stack ghc -- -O2 Socket.hs && ./Socket
它将产生 10 个等待的 forkIO 线程,然后进入一个 getLine/putStrLn 循环,该循环会将您的输入回显给您。同时,您可以使用 netcat 或您喜欢的网络工具向等待线程发送请求:
$ echo -n 'request' | nc -uw0 localhost 6667
这也将被服务器回显。 10次请求后,等待线程已经耗尽,不再响应网络请求。
然后您可以使用fork = 10000 增加线程以创建 10000 个等待线程。在他们等待期间,getLine/putStrLn 主循环将继续运行而不会出现故障。
所有这些都发生在单个 O/S 线程中,您可以通过查看 ps -Lf 或其他内容来验证。
如果同时使用多个套接字并且程序使用-threaded 编译,是否需要更多线程的评论出现了问题。以下测试程序:
import Control.Monad
import Control.Concurrent
import Network.Socket hiding (recv)
import Network.Socket.ByteString
import qualified Data.ByteString.Char8 as BS
forks = 50
main = withSocketsDo $ do
forM_ [0..forks-1] $ \i -> forkIO $ do
s <- socket AF_INET Datagram defaultProtocol
bind s (SockAddrInet (6667+i) (tupleToHostAddress (127,0,0,1)))
BS.putStrLn =<< recv s 4096
let loop = (putStrLn =<< getLine) >> loop
loop
在端口 6667 到 6716 上创建 50 个单独的套接字并等待它们。如果在没有线程的情况下编译,它可以毫无困难地在一个 O/S 线程中运行。如果使用线程编译并提供大于 1 的功能计数,如下所示:
$ stack ghc -- -O2 -threaded Socket.hs
$ ./Socket +RTS -N4
它似乎运行了 11 个工作线程(我认为这是一个“主线程”、四个功能,加上 RTS 源中的常量 MAX_SPARE_WORKERS 指定的六个备用工作线程),它们共享等待这 50 个套接字。
顺便说一句,在Network.Socket 代码中实现这一点的方式是,例如,recv 调用最终实现为:
throwSocketErrorWaitRead sock "..." $
c_recv s (castPtr ptr) (fromIntegral nbytes) 0
throwSocketErrorWaitRead 包装器定义为:
throwSocketErrorWaitRead :: (Eq a, Num a) =>
Socket -> String -> IO a -> IO a
throwSocketErrorWaitRead sock name io =
throwSocketErrorIfMinus1RetryMayBlock name
(threadWaitRead $ fromIntegral $ fdSocket sock)
io
和throwSocketErrorIfMinus1RetryMayBlock 记录如下:
throwSocketErrorIfMinus1RetryMayBlock
:: (Eq a, Num a)
=> String -- ^ textual description of the location
-> IO b -- ^ action to execute before retrying if an
-- immediate retry would block
-> IO a -- ^ the 'IO' operation to be executed
-> IO a
这有点复杂,但结果是包装器调用c_recv 是实际的recv 系统调用。它从不阻塞,因为套接字被配置为非阻塞,如果它返回一个错误代码表明它会阻塞,threadWaitRead 调用用于提醒 RTS 这个绿色线程应该休眠直到数据可供读取。