以下程序创建两个并发运行的线程,在将一行文本打印到stdout之前,每个线程都会随机休眠一段时间。
import Control.Concurrent
import Control.Monad
import System.Random
randomDelay t = randomRIO (0, t) >>= threadDelay
printer str = forkIO . forever $ do
randomDelay 1000000 -- μs
putStrLn str
main = do
printer "Hello"
printer "World"
return ()
输出通常类似于
>> main
Hello
World
World
Hello
WoHrelld
o
World
Hello
*Interrupted
>>
如何确保一次只有一个线程可以写入stdout?这似乎是STM应该擅长的事情,但是对于某些STM a
,所有STM事务必须具有a
类型,并且打印到屏幕的操作具有类型IO a
似乎没有办法将IO
嵌入STM
。
答案 0 :(得分:13)
使用STM处理输出的方法是拥有一个在所有线程之间共享的输出队列,并由单个线程处理。
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad
import System.Random
randomDelay t = randomRIO (0, t) >>= threadDelay
printer queue str = forkIO . forever $ do
randomDelay 1000000 -- μs
atomically $ writeTChan queue str
prepareOutputQueue = do
queue <- newTChanIO
forkIO . forever $ atomically (readTChan queue) >>= putStrLn
return queue
main = do
queue <- prepareOutputQueue
printer queue "Hello"
printer queue "World"
return ()
答案 1 :(得分:4)
使用STM
无法锁定您描述的方式。这是因为STM
基于乐观锁定,因此每个事务必须在任何时候都可以重新启动。如果您将IO
操作嵌入STM
,则可以多次执行。
对于此问题,最简单的解决方案可能是使用MVar
作为锁定:
import Control.Concurrent
import Control.Concurrent.MVar
import Control.Monad
import System.Random
randomDelay t = randomRIO (0, t) >>= threadDelay
printer lock str = forkIO . forever $ do
randomDelay 1000000
withMVar lock (\_ -> putStrLn str)
main = do
lock <- newMVar ()
printer lock "Hello"
printer lock "World"
return ()
在此解决方案中,锁定作为参数传递给printer
。
有些人更喜欢将锁声明为top-level global variable,但目前这需要unsafePerformIO
并且依赖于GHC的属性,AFAIK不属于Haskell语言报告(特别是它依赖于事实上,在执行程序期间,最多只评估一次具有非多态类型的全局变量。
答案 2 :(得分:4)
基于Petr Pudlák's answer的一些研究表明,Control.Concurrent.Lock包中有一个模块concurrent-extra,它提供了基于MVar ()
的锁的抽象。
使用该库的解决方案是
import Control.Concurrent
import qualified Control.Concurrent.Lock as Lock
import Control.Monad
import System.Random
randomDelay t = randomRIO (0, t) >>= threadDelay
printer lock str = forkIO . forever $ do
randomDelay 1000
Lock.with lock (putStrLn str)
main = do
lock <- Lock.new
printer lock "Hello"
printer lock "World"
return ()
答案 3 :(得分:0)
这是Petr提到的使用全局锁的示例。
import Control.Concurrent
import Control.Monad
import System.Random
import Control.Concurrent.MVar (newMVar, takeMVar, putMVar, MVar)
import System.IO.Unsafe (unsafePerformIO)
{-# NOINLINE lock #-}
lock :: MVar ()
lock = unsafePerformIO $ newMVar ()
printer x = forkIO . forever $ do
randomDelay 100000
() <- takeMVar lock
let atomicPutStrLn str = putStrLn str >> putMVar lock ()
atomicPutStrLn x
randomDelay t = randomRIO (0, t) >>= threadDelay
main = do
printer "Hello"
printer "World"
return ()
答案 4 :(得分:0)
尽管newtype Lock = Lock (TVar Status)
data Status = Locked | Unlocked
newLocked :: IO Lock
newLocked = Lock <$> newTVarIO Locked
newUnlocked :: IO Lock
newUnlocked = Lock <$> newTVarIO Unlocked
-- | Acquire a lock.
acquire :: Lock -> IO ()
acquire (Lock tv) = atomically $
readTVar tv >>= \case
Locked -> retry
Unlocked -> writeTVar tv Locked
-- | Try to acquire a lock. If the operation succeeds,
-- return `True`.
tryAcquire :: Lock -> IO Bool
tryAcquire (Lock tv) = atomically $
readTVar tv >>= \case
Locked -> pure False
Unlocked -> True <$ writeTVar tv Locked
-- | Release a lock. This version throws an exception
-- if the lock is unlocked.
release :: Lock -> IO ()
release (Lock tv) = atomically $
readTVar tv >>= \case
Unlocked -> throwSTM DoubleRelease
Locked -> writeTVar tv Unlocked
data DoubleRelease = DoubleRelease deriving Show
instance Exception DoubleRelease where
displayException ~DoubleRelease = "Attempted to release an unlocked lock."
-- | Release a lock. This version does nothing if
-- the lock is unlocked.
releaseIdempotent :: Lock -> IO ()
releaseIdempotent (Lock tv) = atomically $ writeTVar tv Unlocked
-- | Get the status of a lock.
isLocked :: Lock -> IO Status
isLocked (Lock tv) = readTVarIO tv
几乎肯定会表现更好,但实际上您可以根据需要使用STM来实现锁。
acquire
release
/ MVar
对需要仔细的屏蔽和异常处理,就像原始的retry
操作一样。该文档建议,但实际上并未声明,STM操作withMVar
可以中断。假设这是正确的,则retry
使用的相同方法将在此处工作。注意:我已经打开GHC ticket来记录getElementById()
的可中断性。