【问题标题】:Blocking Threads in HaskellHaskell 中的阻塞线程
【发布时间】:2018-08-19 01:26:47
【问题描述】:

我开始使用 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,在其他避免阻塞的情况下,我们在 Future 对象中有回调的概念,其中线程A 可以创建一个线程B 并接收一个Future

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

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

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

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

问候

【问题讨论】:

  • Haskell 线程非常便宜,因为运行时可以在单个操作系统线程上运行其中的许多线程。它们确实是绿线。通常,使线程阻塞是最简单和最有效的选择——无论如何,运行时将为其他绿色线程重用 OS 线程,因此不会丢失任何内容。如上所述,使用MVars 或TVars(或任何其他并发原语)同步绿色线程,如果您需要更多并发,不要害怕多次调用forkIO(即使是短任务) .
  • 所以你说,例如在我的 Http 服务器实现中,当我收到请求时,不是在操作系统中运行,而是在绿色线程中运行,所以程序可以继续接收请求?如果是这样,它会自动执行。
  • 因为在我的示例中,操作系统线程是创建绿色线程并阻塞直到绿色线程完成的线程
  • 在上面的代码中有太多的同步。产生了一个新线程,原始线程立即等待它完成 (takeMVar)。这是没有意义的,因为它是。相反,如果原始线程在forkIO 之后和takeMVar 之前做了一些事情,它可能会很有用。
  • RTS 并不真正执行阻塞系统调用——它使用 POSIX 系统中的 select/poll 之类的东西来等待 IO 并将其分派给等待线程。此外,它每隔 N 毫秒左右在绿色线程之间执行“上下文切换”。

标签: haskell scotty


【解决方案1】:

好的,这里有很多东西要解压。首先,让我们讨论一下您的具体代码示例。为 Scotty 编写 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 链接立即返回,即使slowpure 链接分别在IO 和纯计算上被阻塞。 (threadDelay 没有什么特别之处——它可能是任何 IO 操作,例如访问数据库或读取大文件或代理到另一个 HTTP 服务器或其他任何东西。)您可以继续为 fast、@ 发起多个附加请求987654332@,和pure,当服务器继续接受更多请求时,慢速将在后台运行。 (pure 计算与slow 计算略有不同——它只会在第一次阻塞,所有等待它的线程将立即返回答案,随后的请求将很快。如果我们欺骗了 Haskell为每个请求重新计算它,或者如果它实际上依赖于请求中提供的某些信息,就像在更现实的服务器中可能出现的情况一样,它的行为或多或少类似于slow 计算。)

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

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

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

因此,所有 Haskell 线程,甚至“主”线程,都是绿色的,并且运行在一种虚拟机之上,无论有多少绿色线程阻塞,该虚拟机都可以在单个 OS 线程之上正常运行不管什么原因。

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

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

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

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

【讨论】:

  • 非常感谢您对 Haskell 如何处理操作系统线程的出色解释,我们只运行绿色线程!。关于在 Scotty 中使用 ForkIO 只是因为我正在学习异步编程
猜你喜欢
  • 2012-05-14
  • 2012-11-08
  • 1970-01-01
  • 2020-05-16
  • 2014-02-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-04
相关资源
最近更新 更多