我正在Haskell中编写CGI脚本。当用户点击“提交”时,Haskell程序在服务器上运行,更新(即读入,处理,覆盖)状态文件。然后读取覆盖有时会导致延迟IO的问题,因为我们可能在读完输入之前生成一个大的输出前缀。更糟糕的是,用户有时会在提交按钮上弹跳,并且该进程的两个实例同时运行,争夺同一个文件!
实施
的好方法transactionalUpdate :: FilePath -> (String -> String) -> IO ()
其中函数('update')从旧文件内容计算新文件内容?假设“更新”是严格的是不安全的,但可以假设它是完全的(对部分更新功能的鲁棒性是一种奖励)。可以同时尝试事务,但是如果文件是由其他人写入的,则没有事务应该能够更新。在竞争文件访问的情况下,事务可以中止。我们可以假设系统范围内唯一的临时文件名来源。
我当前的尝试写入临时文件,然后使用系统复制命令覆盖。这似乎解决了懒惰的IO问题,但它并没有让我对比赛安全。是否有一个经过试验和测试的配方,我们可以装瓶?
答案 0 :(得分:6)
最常用的unixy方法是使用flock:
答案 1 :(得分:3)
这是一个粗略的第一个切割,它依赖于底层mkdir
的原子性。它似乎符合规范,但我不确定它是多么强大或快速:
import Control.DeepSeq
import Control.Exception
import System.Directory
import System.IO
transactionalUpdate :: FilePath -> (String -> String) -> IO ()
transactionalUpdate file upd = bracket acquire release update
where
acquire = do
let lockName = file ++ ".lock"
createDirectory lockName
return lockName
release = removeDirectory
update _ = nonTransactionalUpdate file upd
nonTransactionalUpdate :: FilePath -> (String -> String) -> IO ()
nonTransactionalUpdate file upd = do
h <- openFile file ReadMode
s <- upd `fmap` hGetContents h
s `deepseq` hClose h
h <- openFile file WriteMode
hPutStr h s
hClose h
我通过添加以下main
并在threadDelay
中间抛出nonTransactionalUpdate
对此进行了测试:
main = do
[n] <- getArgs
transactionalUpdate "foo.txt" ((show n ++ "\n") ++)
putStrLn $ "successfully updated " ++ show n
然后我用这个脚本编译并运行了一堆实例:
#!/bin/bash
rm foo.txt
touch foo.txt
for i in {1..50}
do
./SO $i &
done
当且仅当相应的号码在foo.txt
时才打印成功更新消息的过程;所有其他人都打印了预期的SO: foo.txt.notveryunique: createDirectory: already exists (File exists)
。
更新:您实际上不想要在此处使用唯一名称;它必须是竞争过程中的一致名称。我已相应更新了代码。