In order to get familiar with STM in Haskell, I wrote the following solution to the Dining Philosophers problem:
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad
import System.Random
type Fork = TVar Bool
type StringBuffer = TChan String
philosopherNames :: [String]
philosopherNames = map show ([1..] :: [Int])
logThinking :: String -> StringBuffer -> STM ()
logThinking name buffer = writeTChan buffer $ name ++ " is thinking..."
logEating :: String -> StringBuffer -> STM ()
logEating name buffer = writeTChan buffer $ name ++ " is eating..."
firstLogEntry :: StringBuffer -> STM String
firstLogEntry buffer = do empty <- isEmptyTChan buffer
if empty then retry
else readTChan buffer
takeForks :: Fork -> Fork -> STM ()
takeForks left right = do leftUsed <- readTVar left
rightUsed <- readTVar right
if leftUsed || rightUsed
then retry
else do writeTVar left True
writeTVar right True
putForks :: Fork -> Fork -> STM ()
putForks left right = do writeTVar left False
writeTVar right False
philosopher :: String -> StringBuffer -> Fork -> Fork -> IO ()
philosopher name out left right = do atomically $ logThinking name out
randomDelay
atomically $ takeForks left right
atomically $ logEating name out
randomDelay
atomically $ putForks left right
randomDelay :: IO ()
randomDelay = do delay <- getStdRandom(randomR (1,3))
threadDelay (delay * 1000000)
main :: IO ()
main = do let n = 8
forks <- replicateM n $ newTVarIO False
buffer <- newTChanIO
forM_ [0 .. n - 1] $ \i ->
do let left = forks !! i
right = forks !! ((i + 1) `mod` n)
name = philosopherNames !! i
forkIO $ forever $ philosopher name buffer left right
forever $ do str <- atomically $ firstLogEntry buffer
putStrLn str
When I compile and run my solution, it seems that no obvious concurrency issues exist: Each philosopher will eventually eat and no philosopher seems to be favoured. However, if I remove the randomDelay
statements from philosopher
, compile and run, the output of my program looks like the following:
1 is thinking...
1 is eating...
1 is thinking...
1 is eating...
2 is thinking...
2 is eating...
2 is thinking...
2 is eating...
2 is thinking...
2 is eating...
2 is thinking...
About 2500 lines later...
2 is thinking...
2 is eating...
2 is thinking...
3 is thinking...
3 is eating...
3 is thinking...
3 is eating...
And so on...
What is happening in this case?
You need to compile it with the threaded runtime and enabled rtsopts
, and run it with +RTS -N
(or +RTS -Nk
where k
is the number of threads. With that, I get output like
8 is eating...
6 is eating...
4 is thinking...
6 is thinking...
4 is eating...
7 is eating...
8 is thinking...
4 is thinking...
7 is thinking...
8 is eating...
4 is eating...
4 is thinking...
4 is eating...
6 is eating...
4 is thinking...
The point is that for another philosopher to think/eat, a context switch must happen if you don't have several hardware threads at your disposition. Such a context switch doesn't happen very often here, where not much allocation is done, so each philosopher has a lot of time to think and eat a lot before the next one's turn comes up.
With enough threads at your disposition, all philosophers can concurrently try to reach for the forks.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With