{-# LANGUAGE PatternSignatures, FlexibleInstances, TypeSynonymInstances #-}
import IO
import Control.Concurrent
import Control.Concurrent.STM
import List
import Join.Base
import Join.Join
{-
Jingle Bells: Solving the Santa Claus Problem in HaskellJoin
In 2003 Nick Benton wrote a paper
"Jingle Bells: Solving the Santa Claus Problem in Polyphonic C".
He shows how to solve a challenging concurrency problem in
Polyphonic C, an experimental extension of C# with concurrency constructs
based on the join calculus. Polyphonic C is no longer maintained (as far
as I know) but instead you can use Claudio Russo's
"The Joins Concurrency Library" which provides the same functionality.
Here, we will revisit the Santa Claus Problem and consider some extensions
to make the problem a bit more challenging. We will use HaskellJoin,
a library to provide join calculus functionality in Haskell.
-------------------------
-- Santa Claus Problem
The original specification says:
Santa repeatedly sleeps until wakened by either all of his nine reindeer,
back from their holidays, or by a group of three of his ten elves.
If awakened by the reindeer, he harnesses each of them to his sleigh, delivers
toys with them and finally unharnesses them (allowing them to go off on holiday).
If awakened by a group of elves, he shows each of the group into his study,
consults with them on toy R\&D and finally shows them each out
(allowing them to go back to work).
Santa gives priority to the reindeer
if there are matching groups of both elves and reindeer waiting.
-------------------------
-- Problem variant
We consider the following variant.
- there are 12 Santas
- 9 deers as before, we only require a group of 3 deers to wake-up
one of the Santas
- 12 elves
We first consider the impact of having several Santas.
Nick's solution incrementally accummulates deers/elves until
we have reached a sufficiently large group of deers/elves.
In case of several Santas this possbily leads to a deadlock.
For example, suppose that Santa1 accummulates 4 and Santa2
accummulates 5 deers.
The fix is to guarantee atomic wake-up by a group of deers
via the join pattern
deer() & deer() & deer() & deer() & deer() & deer() &
deer() & deer() & deer() = deersReady()
The join pattern semantic guarantees that the elements
matching the left-hand side are consumed all at once
or not at all. Thus, we avoid the above deadlock.
In our solution we only require three deers to form a group.
Hence, we find
deer() & deer() & deer() = deersReady()
elf() & elf() & elf() = elvesReady()
We yet need to wake up a santa if there's a group
of three deers or elves waiting (ready).
Each santa carries a unique id for indentification.
santa(id) & deersReady() = "deliver toys"
santa(id) & elvesReady() = "show study and consult on R&D"
Are we done yet?
The specification says:
"Santa gives priority to the reindeer
if there are matching groups of both elves and reindeer waiting."
Experiments (on my mac dual-core) show that the ratio between
the number of groups of deers to groups of elves (which wake-up a santa)
is one to two (1:2).
How can we give higher priority to a group of deers?
We're happy with an approximate solution. Each santa first checks
for a group of deers before considering a group of elves. It's clear
that a fresh group of deers may become available just right after
we've chosen a group of deers. We're have to accept such choices.
They are unavoidable in a highly concurrent setting where resources
become (un)available at any point in time.
In HaskellJoin, join patterns are tried from top to bottom order
(like in other pattern matching based languages like
Prolog and Haskell). Hence, we order join pattern clauses
as follows.
santa(id) & deersReady() = "deliver toys"
santa(id) & elvesReady() = "show study and consult on R&D"
deer() & deer() & deer() = deersReady()
elf() & elf() & elf() = elvesReady()
The order of the last two clauses is irrelevant.
But it's important to try clause
santa(id) & deersReady() = "deliver toys"
before trying
santa(id) & elvesReady() = "show study and consult on R&D"
However, this doesn't yet achieve the desired effect.
For example, suppose we have
- a sleeping santa, santa(1)
- a group of deers waiting, deersReady()
- a group of elves waiting, elvesReady()
Each deersReady() and elvesReady()
try the above clauses in top to bottom order.
(a) deersReady() can trigger the first clause in combination with santa(1)
(b) elvesReady() can trigger the second clause in combination with santa(1)
We have no control which computation will take place first (eg there
are many run-time factors involved, number of cores, threads, context
switching time etc). Computation of (a) will invalidate (b) and vice versa.
How can we give preference to (a)?
The trick is to include the following clause
elvesReady() & santa(id) & deersReady() = do elvesReady()
"deliver toys"
If there's a group of elves and deers ready, we prefer
the group of deers. Because we haven't selected the group
of elves, we call elvesReady again in the join body.
santa(id) & deersReady() = "deliver toys"
elvesReady() & santa(id) & deersReady() = do elvesReady()
"deliver toys"
santa(id) & elvesReady() = "show study and consult on R&D"
deer() & deer() & deer() = deersReady()
elf() & elf() & elf() = elvesReady()
Experiments show that the ratio between the number of groups of deers
to groups of elves (which wake-up a santa) is now one to one (1:1).
Minor point:
The consecutive removal and addition of the same element elvesReady()
seems wasteful. In HaskellJoin, we can express propagation of join pattern elements
by writing them to the left of \. Elements to the right
will be removed.
elvesReady() \ santa(id) & deersReady() = "deliver toys"
Comparison to Nick's solution to enforce priorities
waittobewoken() & elvesReady() & deerNotReady() = deerNotReady ...
-- we could use propagation
-- deerNotReady() \ waittobewoken() & elvesReady() = ...
-- to allow that multiple elf groups can execute concurrently
-- (if the deers are not ready yet)
deer() & deer() & deer() = do clearDeerNotReady()
deerReady()
clearDeerNotReady() & deerNotReady() = return ()
waittobewoken() & deerReady() = deerNotReady
Points to note:
- a group of elves must explicitely synchronize with a deerNotReady token.
It's unclear how to extend Nick's priority solution to our variant
of 12 santas, 9 deers and 12 elves.
- Nick's priority solution doesn't depend on the (top to bottom) execution order.
He claims a fixed execution order may prevent optimization.
Our (implementation) experiences show that this is not the case.
A valid argument is that top to bottom execution order leads to non-modular code.
References:
http://research.microsoft.com/~nick/polyphony/
http://research.microsoft.com/~crusso/joins/
http://research.microsoft.com/~nick/santa.pdf
-}
-- patterns
santaPat id = method "Santa" id
deerPat = method "Deer" ()
elfPat = method "Elf" ()
readyDeersPat = method "readyDeers" ()
readyElvesPat = method "readyElves" ()
-- method calls (all asynchronous)
santa :: Join -> Int -> IO ()
santa join id = --callPassive
call
join "Santa" id
elf :: Join -> IO ()
elf join = call join "Elf" ()
deer :: Join -> IO ()
deer join = do
call join "Deer" ()
readyDeers :: Join -> IO ()
readyDeers join = call join "readyDeers" ()
readyElves :: Join -> IO ()
readyElves join = call join "readyElves" ()
santaJoinDefinitions output (cntE, cntD, cntD2) join activeMethod = do
c . do
v_c deer join)
[1..3]
santa join v_c
, [readyElvesPat] .\. [santaPat c, readyDeersPat]
.->. do
v_c deer join)
[1..3]
santa join v_c
,[santaPat c, readyElvesPat]
.->. do v_c elf join) [1..3]
santa join v_c
,[deerPat , deerPat , deerPat]
.->. readyDeers join
, [elfPat, elfPat, elfPat]
.->. readyElves join
]
res action
Nothing -> return ()
-- testing
printOutput o = do
do w atomically $ newTVar 0) [1..3]
let join = Join {store = jStore, rules = santaJoinDefinitions output (c1,c2,c3)}
let elfNo = 18
let deerNo = 9
let santaNo = 12
mapM_ (\_ -> elf join) [1..elfNo]
mapM_ (\x -> deer join) [1..deerNo]
mapM_ (\s -> santa join s) [1..santaNo]
let printLoop = do
(a,b,c)