CS463 - Homework 9 - Game of Functional Thrones
Due Tuesday, April 23 2024, 5:00pm
Table of Contents
Quick Overview
- We expect identical output to last time, so we will even use the same consistency checker script: chairs_checker.py
- you may work with one partner this time.
- You may share everything except the writeup.
- Each Turn In Your Work. This is primarily as a backup in case
your partner fails to turn things in appropriately for you, or
someone wants to work into the late period.
- Please explain in your writeup how similar your work is to your partner's so we can speed up grading.
- start with the Required Components to begin your file.
Suggested Resources
Look at our class examples on concurrency in Haskell for a good
starting point. If you're unsure what approach to use, I'd suggest
using simple MVar
's, and perhaps some Chan
's as you see fit. If
you want some more longform introduction, look to our readings on the
schedule covering Monads/IO (LYAHFGG, RWH). When you want to find a
built-in function, type in the signature to hoogle and see if you can
find an existing definition. In contrast, I'd strongly suggest not
using the STM
monad (software transactional memory), as it's a lot
harder to get correct, and mixing it with the other monads is a much
more complex undertaking than our class has prepared your for.
Description / Review
We are re-implementing our Musical Chairs assignment in Haskell. We
will most likely use the MVar
, Chan
, and/or BoundedChan
types;
we will create threads for players and the emcee (via forkIO
or
async
), just as before; and chairs need to be separate resources
(e.g. different MVar
's or some other shared things), just as
before. We will target the same output as before, too. In short, use
Haskell to conform to the same rules as in the previous musical chairs
assignment.
Honestly, that should be about enough to define it. But we will review what the layout is, and then give some Haskell-specific suggestions to help you get started.
The Task: Musical Chairs
N
players want to sit on (N-1)
chairs. An emcee (the announcer)
turns the music on and off, and comments on who wins, who loses, and
so on. Each round, when the music turns off, all active players try to
sit in the chairs, but one will not be able to find a chair. The emcee
announces that that player has lost, and the next round begins with
one fewer chairs and one fewer players, until there is one winner.
- Requirements
- the number of players is the first command-line argument to the
whole program. When not present, your code must default to
N = 10
. (See our class example,Args.hs
, for handling command line arguments). - The emcee is a separate thread, which exists for the duration of the game. (it is okay to use the main thread for the emcee).
- Each player is a separate thread. Each player has a 'name', from
P1
throughPN
. These player threads must exist for the duration of the game, they are not recreated each round. (It's okay for these threads to die when they lose - no need to hang around afterwards). - Each chair is a separate resource (e.g., an object). The chairs
are named
C1
throughC(N-1)
. It must be possible for multiple players to obtain different chairs at the same time.- If you have any sort of a single lock on all chairs (meaning that only one player at a time can access them) then you are not fulfilling the requirements of this assignment. A common example violation of this goal would be that the chairs are not just in an array, but that the only way to find a chair to sit on is through some method call that 'controls' the array, or if the entire array is synchronized/locked while an individual is looking for a chair. Instead, each item in the array would need to be separately lockable on their own.
- each round, whichever player did not manage to obtain a chair is out. The remaining player threads get to play in the next round, and the highest-numbered chair is removed. This means that we always have chairs C1 through C(k-1), but not necessarily players P1 through Pk. For example, in the last round, it's always chair C1, but it could/should be any two of the original players vying for the win.
- it's entirely up to you to decide what algorithm your players use to find chairs, and it does not need to be fancy (start simple, enhance it later perhaps?). They may rely upon the numbering system of the chairs and their own numbers, or any other strategy that you can design. As long as each player is able to find each open chair eventually, it is fine. Writing up this approach is part of the required (short) document at the end.
- What can I Import? - the only limitation on what you may import
from standard libraries is any randomization library; the
variability between runs we see needs to be from threading, not
from a random number generator. Other than that, any concurrency
libraries or data structure libraries can all be used if you have
explored and learned about things in your language beyond what we
see in class. Since it has to be a standard library, we won't see
anything like
import com.somewebsite.musicalChairLib.solution;
it's still gotta be your work solving the actual task.
- the number of players is the first command-line argument to the
whole program. When not present, your code must default to
- Notes and Suggestions
- It is okay to include some extra coordination points as necessary between the emcee and players, as long as the find-a-seat phase is initiated by changing the music and the players are able to access different chairs at the same time. In a real game of musical chairs, the emcee would somehow have to inspect all the chairs and identify the person who isn't sitting, so there's a bit of a linear computation to be done here and there.
- Though not required by your assignment, I added a second
command-line argument for an output file name. When omitted,
everything is printed to standard out, but when present, I save the
contents to that file. This was occasionally helpful to further
inspect outputs from various runs of the program and not need to
keep scrolling back in the terminal.
- printing interleaved messages can be difficult in various
languages, because multiple threads are competing for the standard
output. If you are having any issues with interlaced characters
from multiple messages, then you need to introduce something to
coordinate the message printing. I tend to have only one resource
(or thread) in charge of actual printing, and everyone else sends
messages they'd like to print to that resource whenever they'd
like. This was the purpose of our
announcer
threads in our class examples.- because most of our program ends up printing things, it's entirely possible that printing may be the bottleneck of our program. Don't worry about that, but if you have details to share in your writeup, be sure to mention it.
- printing interleaved messages can be difficult in various
languages, because multiple threads are competing for the standard
output. If you are having any issues with interlaced characters
from multiple messages, then you need to introduce something to
coordinate the message printing. I tend to have only one resource
(or thread) in charge of actual printing, and everyone else sends
messages they'd like to print to that resource whenever they'd
like. This was the purpose of our
- It's not just expected that each run will produce varied outcomes and varied orders of sitting, but keep in mind that the two separate actions of find-chair and print-sat-message also may be further apart in time than you expect. A later sat-message doesn't mean that's when they sat, only that that's when the message got printed! Be careful not to assume too much from the order of printed messages when debugging. This can trick us into some dead-end debugging sessions.
- Much more so than in single-threaded programming, if you have a ton
of debug-style print statements, they will affect the timing of
your program. Removing them can absolutely uncover some nasty race
condition bugs.
- I highly recommend regularly running your code with no debugging statements to avoid finding race conditions at the last minute.
- Required outputs
In order to facilitate grading, you need to print these messages (and only these messages) to standard output, in the indicated ordering. Each one is printed on a line by itself. You may introduce blank lines as desired; they will be ignored.
- the emcee indicates the game is beginning with
BEGIN N players
. This is always the first message of the whole game, with the actual positive integer value replacingN
, e.g.BEGIN 5 players
. - each round, the following happens:
- the emcee prints "round X", e.g.
round 1
,round 13
, etc. The music turns on at this point (no printed message about turning the music on exists - that's implied by the round starting). - after the "round" message has printed and all players are ready
to begin, the emcee thread prints
music off
and then immediately turns off the music. Players must notice the music is not playing in order to know that they may begin searching for a seat. - as players find and take ownership of available chairs, messages
of the form "
P1 sat in C1
" must be generated and printed. Of course the correct player and chair numbers must be used. - the last player realizes they have lost (perhaps by the emcee
telling them), and the last message of the round prints, e.g.:
P1 lost
.
- the emcee prints "round X", e.g.
- when there is only one player left, instead of printing
round X
again, the emcee announces the winner with a message, e.g.:P1 wins!
. - the emcee indicates the game is over with
END
. This is always the last message of the whole game.
- the emcee indicates the game is beginning with
- to interpret it, we use the
runhaskell
executable. Example:runhaskell Homework9 3
- to compile it, we create an executable like this example:
ghc -threaded -main-is Homework9 -o h9 Homework9.hs
- we explicitly ask to have an executable that can do
multithreading (
-threaded
) - we indicate which module has the
main::IO()
function that should be the program with-main-is Homework9
- we name the output using
-o h9
- we explicitly ask to have an executable that can do
multithreading (
- to run our compiled code utilizing a specific number of cores:
./h9 10 +RTS -N4 -RTS
./h9
is the executable10
is a plain old command line argument (number of players)- We sandwich any arguments to the runtime system between
+RTS
and-RTS
. We include-N4
to request four HECs ("Haskell Executable Contexts", roughly meaning number of cores).
- Structure Notes:
- what is the shared resource that represents the music? And specifically, where (file/line numbers) does the emcee turn on/off the music?
- where (file/line) do you create the threads for each player?
- what value do you use to represent each chair? Where (file/line) are they all created?
- What was your strategy for players to find empty chairs? How does this ensure all chairs are eventually found?
- For our programming task, what were the challenges that you faced? Where was there competition for resources, and where was there a need for cooperation?
- What aspects of the task were straightforward, and which ones felt difficult or laborious?
- What kinds of bugs did you run into – deadlock? Ordering
issues? How did you attempt to inspect what was going wrong
with the code? For example, did you try
Debug.trace
? (Did you use any debuggers or anything? Not required, I'm just curious how your experience went with various languages, and I want you to debrief on it). - Any extra thoughts or comments?
Sample Runs
We can both interpret our code, or compile and run our code.
Here are some sample runs. Of course you're not expected to exactly match the order of who sits, or match who loses each round.
demo$ runhaskell Homework9 3 BEGIN 3 players round 1 music off P3 sat in C2 P2 sat in C1 P1 lost round 2 music off P3 sat in C1 P2 lost P3 wins! END
demo$ runhaskell Homework9 3 BEGIN 3 players round 1 music off P3 sat in C2 P2 sat in C1 P1 lost round 2 music off P2 sat in C1 P3 lost P2 wins! END
demo$ ghc -threaded -main-is Homework9 -o h9 Homework9.hs Loaded package environment from <...> [1 of 2] Compiling Homework9 ( Homework9.hs, Homework9.o ) [Source file changed] [2 of 2] Linking h9 [Objects changed] <...> demo$ ./h9 3 +RTS -N4 -RTS BEGIN 3 players round 1 music off P1 sat in C1 P2 sat in C2 P3 lost round 2 music off P2 sat in C1 P1 lost P2 wins! END
demo$ runhaskell Homework9 6 BEGIN 6 players round 1 music off P6 sat in C2 P4 sat in C1 P1 sat in C4 P3 sat in C5 P5 sat in C3 P2 lost round 2 music off P3 sat in C2 P4 sat in C4 P6 sat in C3 P1 sat in C1 P5 lost round 3 music off P3 sat in C1 P1 sat in C2 P4 sat in C3 P6 lost round 4 music off P3 sat in C2 P1 sat in C1 P4 lost round 5 music off P3 sat in C1 P1 lost P3 wins! END demo$
Testing It
I've provided a consistency checker file: chairs_checker.py
If you correctly generate output, this code will consume the output and confirm that it is valid or not; this allows for the expected variance in who wins each time but checks for things like players staying out of the game once they lose, correct number of rounds, and so on.
It's written in python 3; you need to give it a single command line argument that is either a filename (containing all the output from a program run), or give it "stdin" indicating it'll receive all the output (likely piped to it).
demo$ java H7 5 > 5.txt demo$ python3 chairs_checker.py 5.txt Everything looks good! demo$ java H7 10 | python3 chairs_checker.py stdin Everything looks good! demo$
It's important that you do not print out extra output when your work is turned in, as this will trash the consistency check.
Required Components
You must have a file named Homework9.hs
, and it must contain a
function main
of type IO()
. There are some suggested imports in
the file stub below, which are safely ignored when not needed.
{- Name: ________________ Partner: _____________ G#: ________________ (other header-comments you'd like to add) -} module Homework9 where import Control.Monad -- many useful functions import Control.Concurrent -- threadDelay, forkIO, MVar..., Chan... import Data.IORef -- newIORef, readIORef, writeIORef import System.Environment -- getArgs import Debug.Trace {- -- download BoundedChan from hackage if you want to use this one. -- You'll get: BC.BoundedChan, BC.newBoundedChan, BC.readChan, BC.writeChan, etc. -- import qualified Control.Concurrent.BoundedChan as BC -} main :: IO () main = do ...
Now that we have a main
function, you can run your file from the
command line as follows without having to compile. This example also
supplies N = 10
as a command line argument.
runhaskell Homework9.hs 10
Write-up
As a last step, you will write up a brief summary of what you
experienced, in a README.txt
file (README.pdf
is also
acceptable). I'd anticipate around a page at most; as long as
you've answered all questions adequately, there's no minimum
limit. Here are the questions to address:
Turning it in
- All Haskell implementations need to have a
Homework9.hs
module with themain
method to run your entire program. - Create a folder named with this convention:
gmason76_h9/
Inside of it, place the following:
- all source code (COMMENTED!)
README.txt
(your write-up; other common document types accepted)- any (pre-approved) modules your code needs outside of the
standard library (such as
BoundedChan
).
- zip that folder, and submit it on GradeScope. Yes, you and your partner both turn it in separately.
Getting Help
Threaded programing can be surprising when we first start. Please begin the assignment early enough that you can ask questions.
Spend more time than you might be used to spend planning your algorithm, thinking through any potential issues, and really mapping out your plans. Don't just code-as-you-go and hope you stumble into a solution.
Also, think about good early milestone versions of your program that can help you ensure certain features or behaviors of your code are working before you tackle the full program.
Just a reminder, collaboration on the assignment with one partner is allowed; you can discuss concurrency issues generally with anyone, but once you're working on musical chairs (either implementation details or thinking through how to solve it in general), you need to restrict yourself to just you, your partner, and the instructors of the course (GTA, professor).
Grading
Points Breakdown:
category | points |
---|---|
Structure | 50 |
Performance | 35 |
Write-up | 15 |
Total | 100 |
Structure: 50
These points go towards the structure and approach of your code. You still need to have compiling, runnable code that is mostly complete in order to earn all these points, but it's possible to get some points here even if you have some later issues.
- +10 - emcee runs the show via stopping music
- just means that the player threads read some variable to begin
seeking chairs. (Hint: perhaps an
MVar
?) - you need to mention this in your writeup for full credit.
- just means that the player threads read some variable to begin
seeking chairs. (Hint: perhaps an
- +10 - each player is its own thread
- find the map/recursion/whatever that creates them, and ensure that this code actually performs the playing task.
- note:
forkIO
is how we create/start threads in Haskell.
- +10 - each chair is its own resource
- e.g., NOT just a list of ints, or a single int like
numChairsLeft
. There should be some linear structure holding all the individual chair resources, which is still okay. - you need to mention your approach in your writeup for full credit.
- e.g., NOT just a list of ints, or a single int like
- +10 - multiple chairs can be accessed at the same time (there is no "global lock").
- + 5 - surviving players preserved for next round
- loser thread is finished/killed, or somehow removed from gameplay while all others continue to the next round, rather than restarting an entire cast of player threads. Creating more player threads is not allowed for successive rounds, even if their player numbers are preserved!
- + 5 - handles variable # of players without crashing due to the number of players value. (not hard-coded to ten players).
Performance: 35
These points are earned by the functionality and behavior of your code.
- +15: various sizes successfully complete the game. (E.g. different
N
values work). - +10: specific sizes repeatedly complete the game in different ways
(E.g. repeated runs of
N
= 5 show different valid results). - +10: all printouts are precisely as required
- especially important: no extra printing in your submitted work beyond the expected messages. Comment out those debugging statements!
Writeup: 15
- three points per question, for the first five questions. (#6 is optional).
Common Deductions
- structure:
- poorly- or un-commented code. (up to -10)
- forgot to mention line numbers of emcee stopping music, where threads are created, or chairs as resources.
- there exists some bottleneck that all chair-acquisition attempts must go through (some "global lock" that becomes a one-at-a-time request).
- performance:
- extra printing other than the required print statements. clean up your code before submitting! (up to -10)
- mis-ordered print statements, e.g.
sat
messages not all received/printed before the next round begins, or alost
message that isn't the last message of the round. (up to -10) - program sometimes, often, or always hangs/crashes without completing the game. (much of the performance points are at stake in this situation)
- Writeup:
- skipped parts of question or didn't answer what was asked.
Programming Hints
- We studied a few different approaches in class:
Control.Concurrent.MVar
provides operationstakeMVar
andputMVar
to get a single-slot, automatically blocking buffer. It also offers non-blocking versionstryPutMVar
andtryTakeMVar
.Control.Concurrent.MChan
provides unbounded space for values to be placed/removed vianewChan
,writeChan
, andreadChan
.BoundedChan
provides the same notion ofMChan
, but at a fixed size (after which it exhibits blocking behavior). We likely don't need the bounding to get a semantically correct implementation but it may fit your idea better.
- think about how you can create a piece and test it individually; do you have a simpler mini-program you'd like to get working, first? Something you can typecheck and run and learn a bit from, and then perhaps directly adapt into the assignment code. (I don't have a secret example in mind but you can often simplify a program and get the same style of code).
- use some sort of centralized authority for printing, like the
announcer in our class examples. It was the sole consumer of an
MVar String
, where all other threads could produce values for theMVar
. Although this announcer's job is toputStrLn
various messages, theMVar
doesn't necessarily have to hold strings - perhaps your own custom datatype could be used here. - Though there is an interesting Software Transactional Memory
library (providing
TMVar
,TVar
,TChan
), it's not really interoperable with our main class examples. I strongly recommend that you don't try to use it on the assignment, and definitely not mixed with the regularMVar
/IORef
/Chan
concurrency primitives.
Multicore usage in Haskell
Tip on using more logical cores in Haskell concurrent code
(This content is from our "installing haskell" text document, but is finally relevant here).
Haskell has a way to specify how many cores you'd like your code to run; these are called "Haskell Executable Contexts" (HECs).
ghc -threaded -main-is Homework9 -o h9 Homework9.hs ./h9 10 +RTS -N4 -RTS
ghc: We first want to compile our code with ghc.
-threaded
: multithreading, please-main-is Homework9
: the main function we want to run isn't in a module named Main, so we specify where that main function is.-o h9
: the output should be named h9.Homework9.hs
: here's the source code.
Then, we run the executable:
./h9
: this thing should be executed. (I'm running on mac and on zeus; on windows, likely you'd just have h9).10
: a normal command line argument.+RTS ... -RTS
: between these is where you can pass "runtime system" arguments.-N4
: the number of HECs should be four, please.
I've run this successfully on my own machine (mac) and on zeus.
If you don't use this approach, it's likely that the way your code's
logical threads get scheduled may be a bit more (too) predictable, but
it's not something you have to do to run haskell code. On large
enough examples, e.g. N=5
or more, you should start seeing a bit of
variability.