3

我编写了一个小型服务器,它接受作为 POST 请求的注册并通过将它们附加到文件来持久化它们。一旦我将此服务器置于负载状态(我使用具有 50 个并发线程和 10 个重复计数的 Apache JMeter,并且帖子包含一个包含约 7k 文本数据的字段),我就会得到很多“资源繁忙,文件是锁定”错误:

02/Nov/2013:18:07:11 +0100 [Error#yesod-core] registrations.txt: openFile: resource busy (file is locked) @(yesod-core-1.2.4.2:Yesod.Core.Class.Yesod ./Yesod/Core/Class/Yesod.hs:485:5)

这是代码的精简版本:

{-# LANGUAGE QuasiQuotes, TemplateHaskell, MultiParamTypeClasses, OverloadedStrings, TypeFamilies #-}

import           Yesod
import           Text.Hamlet
import           Control.Applicative ((<$>), (<*>))
import           Control.Monad.IO.Class (liftIO)
import           Data.Text (Text, pack, unpack)
import           Data.String
import           System.IO (withFile, IOMode(..), hPutStrLn)

data Server = Server

data Registration = Registration
        { text      :: Text
        }
    deriving (Show, Read)

mkYesod "Server" [parseRoutes|
/reg    RegR    POST
|]

instance Yesod Server

instance RenderMessage Server FormMessage where
    renderMessage _ _ = defaultFormMessage

postRegR :: Handler Html
postRegR = do
    result <- runInputPost $ Registration
        <$> ireq textField "text"
    liftIO $ saveRegistration result
    defaultLayout [whamlet|<p>#{show result}|]

saveRegistration :: Registration -> IO ()
saveRegistration r = withFile "registrations.txt" AppendMode (\h -> hPutStrLn h $ "+" ++ show r)

main :: IO ()
main = warp 8080 Server

我故意编译了没有 的代码-threaded,并且操作系统只显示一个正在运行的线程。尽管如此,在我看来,请求并没有完全序列化,并且在旧请求写入磁盘之前已经处理了一个新请求。

您能告诉我如何避免错误消息并确保成功处理所有请求吗?性能还不是问题。

4

2 回答 2

4

Handle从多个线程写入 a 是完全可以的。事实上,Handles 里面有MVars 来防止奇怪的并发行为。您可能想要的不是MVar手动处理 [sic] s(例如,如果处理程序抛出异常,这可能导致死锁),而是withFile在各个处理程序线程之外解除调用。该文件一直保持打开状态 - 在每个请求上打开它都会很慢。

我对 Yesod 不太了解,但我会推荐这样的东西(可能无法编译):

data Server = Server { handle :: Handle }

postRegR :: Handler Html
postRegR = do
    h <- handle `fmap` getYesod
    result <- runInputPost $ Registration
        <$> ireq textField "text"
    liftIO $ saveRegistration h result
    defaultLayout [whamlet|<p>#{show result}|]

saveRegistration :: Handle -> Registration -> IO ()
saveRegistration h r = hPutStrLn h $ "+" ++ show r

main :: IO ()
main = withFile "registrations.txt" AppendMode $ \h -> warp 8080 (Server h) 
-- maybe there's a better way?

另外:如果您想异步写入文件,您可以写入队列(如果它是日志文件或其他内容),但在您的用例中,您可能希望让用户知道他们的注册是否失败,所以我会建议使用此表格。

于 2013-11-02T18:51:29.193 回答
3

即使没有-threadedHaskell 运行时,也会有几个“绿色线程”协同运行。您需要使用Control.Concurrent来限制对文件的访问,因为您不能同时有多个线程写入它。

最简单的方法是MVar ()在您的文件中添加一个,Server并让每个请求在打开文件之前从“获取”该单元MVar,然后在文件操作完成后将其放回原处。bracket即使写入文件失败,您也可以使用它来确保释放锁。例如像

import Control.Concurrent
import Control.Exception (bracket_)

type Lock = MVar ()
data Server = Server { fileLock :: Lock }

saveRegistration :: Registration -> Lock -> IO ()
saveRegistration r lock = bracket_ acquire release updateFile where
    acquire = takeMVar lock
    release = putMVar lock ()
    updateFile =
        withFile "registrations.txt" AppendMode (\h -> hPutStrLn h $ "+" ++ show r)
于 2013-11-02T18:06:43.437 回答