【问题标题】:Is Network.Socket programming in Haskell "Concurrently, Asynchronously, Parallel, Non-Blocking" using forkIOHaskell中的Network.Socket编程是使用forkIO“并发,异步,并行,非阻塞”吗
【发布时间】:2018-09-20 07:33:27
【问题描述】:

我想了解 GHC 的 RTS 是否同时处理阻塞读/写操作,即假设我假设的服务器只能并行处理 CPU 1000 个线程,如果所有 1000 个 forkIO 由于持久客户端套接字阻塞读/写而被阻塞,并且还有 500 个请求需要处理。

Option-A>另外500请求是否要等到1000 forkIO进程完成。

Option-B> Haskell 通过有效地使用所有 1000 个 CPU 线程,在内部处理(即并发、异步、并行、非阻塞)1000 + 500 个 forkIO。

仅供参考,我已经阅读了 C 和 Haskell(Network.Socket) 的许多套接字教程(和博客),以了解 Haskell(forkIO) 和 C 的处理方式(即并发、异步、并行、非阻塞),但我对 Haskell 是如何做到这一点并不清楚。

参考:

https://github.com/lpeterse/haskell-socket/issues/15#issuecomment-224382491

【问题讨论】:

    标签: multithreading sockets haskell


    【解决方案1】:

    请注意,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 这个绿色线程应该休眠直到数据可供读取。

    【讨论】:

    • 我认为你在这里不够小心。 Network.Socket 只允许一个操作系统线程处理每个套接字,据我所知。它确实执行 GHC I/O 子系统所做的那种特殊处理,以使人们相信多个线程可以同时执行此操作。因此,您需要一个绑定线程(使用forkOSforkOnasyncBoundasyncOn 等创建)来代表其他线程管理每个套接字。
    • 我不认为这是正确的——我可以修改程序以在 10 个不同的端口上打开 10 个不同的套接字,并且它的工作原理几乎相同——所有东西都在一个 O/S 线程中运行,永远不会分叉并花费大部分时间等待 select 调用 stdin 和 10 个套接字。
    • 哦,因为你编译它是为了在没有线程的情况下运行。但是,如果这是需要线程的更大程序的一部分,这似乎不是一个好主意!我认为最好编写代码,这样它也可以在该上下文中工作。
    • 即使它是用-threaded+RTS -N4 编译的,仍然没有一个O/S-thread-per-socket 规则。 RTS 将在一个池中启动超过 4 个 O/S 线程,但数量仍然有上限。如果我打开 50 个套接字,我会得到 11 个 O/S 线程。我相信超出部分是由 RTS 源中的 MAX_SPARE_WORKERS 常量决定的,它允许一定数量的 O/S 线程超过能力计数。无论如何,我已将来源添加到答案中。
    • 听起来我们可能正在互相交谈,除非我只是错过了一些东西。 Haddocks 有一个注释:“正确的编程模型是一个 Socket 由单个线程处理。如果多个线程同时使用一个 Socket,就会发生意想不到的事情。多线程与一个线程有一个例外。单个Socket:一个线程仅从Socket 读取数据,另一个线程仅将数据写入Socket。”
    猜你喜欢
    • 2011-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-30
    • 1970-01-01
    • 2018-11-15
    • 2013-05-07
    • 2018-04-06
    相关资源
    最近更新 更多