我编写了一个小型服务器,它接受注册为POST请求,并通过将它们附加到文件来保留它们。一旦我把这个服务器加载(我使用Apache JMeter有50个并发线程,重复计数为10,并且帖子包含一个带有~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
的情况下故意编译了代码,操作系统只显示了一个运行的线程。尽管如此,它看起来像请求没有完全序列化,并且在将旧请求写入磁盘之前已经处理了新请求。
您能否告诉我如何避免错误消息并确保所有请求都成功处理?性能还不是问题。
答案 0 :(得分:4)
从几个线程写入Handle
是完全可以的。实际上,Handle
内部有MVar
个,以防止奇怪的并发行为。您可能想要的是不要手动处理[sic] MVar
(例如,如果处理程序抛出异常会导致死锁)但是将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?
除此之外:如果您想要异步写入文件,您可以写入队列(如果它是日志文件或其他东西),但在您的用例中,您可能希望让用户知道他们的注册是否失败,所以我建议保留此表格。
答案 1 :(得分:3)
即使没有-threaded
,Haskell运行时也会有几个“绿色线程”协同运行。您需要使用Control.Concurrent
来限制对文件的访问,因为您不能同时有多个线程写入它。
最简单的方法是在Server
中添加MVar ()
并让每个请求在打开文件之前从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)