(Latest Revision: Mon Nov 5 19:45:09 PST 2001 )
////////////////////////////////////////////////////// CS 3750 Second C Programming Assignment (prog #2) ////////////////////////////////////////////////////// Before starting this assignment, read the file heapsort.pas to review what is involved in a heapsort. Specifically, it is the intial heap-building activity (heapification) that is the subject of this assignment. File heapsort.pas is written in the Pascal programming language. I'll give you some tips for how to read it. Use queuing semaphores to implement a "parallel heapification" program that turns a complete binary tree with 31 elements into a heap. Below you see a depiction of a 31-element binary tree. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * From reading heapsort.pas, you can learn all the details of what a heapification algorithm *does*, but please keep in mind that the code in heapsort.pas is very *unlike* the code you need to write for a *parallel* heapification. I want your program to have a separate thread to play the part of each internal node in the tree. The mother thread can play the role of the root node, if you like. That leaves 14 threads to be created by the mother to assume the roles of the other internal nodes. You can have threads to play the roles of the leaf nodes if you want, but you don't have to. The threads must communicate using shared semaphores and variables. Semaphores implemented by sem.cpp and sem.h are the only synchronization primitives you are allowed to use in this program (with the sole exception of a mutex_t called stdoutLock for locking standard output, as you were allowed in the last program.) Each internal node-thread will be responsible for starting and propagating "waves" of re-heap-down activity from the node that it represents. It will be responsible for receiving information about completed operations from child nodes, and for using information received as a basis for sending information on to its parent. P \ \ \ A / \ / \ / \ L R The communication between the threads could be done solely with semaphores. In this mode, it would be best to use different semaphores for different messages to make it easier for the programmer and others (i.e. me) to understand the logic of the program. For example there could be semaphore arrays NeedReHeap[] and ReHeapDone[] set up so that each internal node has one element of each array associated to it. A process would SIGNAL on the semaphore of another process when it wants to send a message. For example, L could SIGNAL on ReHeapDone[A] when it wants to tell A that the subtree rooted at L is now a heap. Conversely, A could WAIT on ReHeapDone[A] when it needs to receive that message. Another modality is for communication between the threads to be primarily done through a simplified message-sending interface, possibly implemented by an array of message buffers: myMessages[], appropriately protected by semaphores in such a way as to guarantee exclusive write-access and to avoid the possibility of a message being overwritten before it is read. One way to do this sort of thing is to have two semaphores for each message buffer. One semaphore is called newMessage[], and the other is called doneReading[]. When a reader wants to read, he WAITs on newMessage[], reads, and then SIGNALs on doneReading[]. When a process wants to send a message, she WAITs on doneReading[], writes the message in the buffer, and SIGNALs on newMessage[]. All critical section problems you encounter must be solved in such a way that the mutual exclusion, progress, and bounded waiting conditions are all guaranteed. All synchronizations done must guarantee freedom from all forms of indefinite postponement. Your program must reflect the inherent parallelism of heapification. In other words, everything that *can* go on in parallel must be *allowed* to go on in parallel. Your program must not "force" tasks to proceed serially if they do not need, for correctness, to proceed serially. The overall task of the program will be to 1. read in a list of 31 numbers from standard input (see sample.in), 2. to create an array containing the numbers, in the order read, 3. to transform the array into a heap using the parallel heapify algorithm, 4. to write some lines of output while doing the heapifying of step 3, and 5. to write the finished heap to the standard output in a specific format. Here is the way the heap is written when the input is that of file sample.in: 717 569 695 541 200 650 489 499 465 051 078 501 602 198 069 076 328 040 012 032 030 043 011 082 072 173 067 004 049 019 059 Write the program so that this part of the output is performed by just one thread. The root thread or the mother thread (if different from the root) is a logical choice for assignment to this job. Assume that the numbers in the input are all integers between 0 and 999 (inclusive). During the heapification process, threads will be doing swaps and communicating to each other. (I discussed ways of communicating above.) I want you to program each thread so that it writes a line to standard output each time it does a swap, and each time it, in effect sends a message to another thread. In the case of a swap, the line of output must identify which thread is doing the swapping, and the identities of the threads whose values are being swapped. In the case of a message, the line of output must indicate the identity of the sender, the identity of the recipient, and the nature of the message. Use integers in the range 1..31 (not 0..30) to identify the threads in these lines of output. In this numbering scheme, the children of node number i are nodes numbered 2i and 2i+1. Here are some sample messages:
Node04 tells Node02: Tree(Node04) is a heap now. Node07 tells Node03: Tree(Node07) is a heap now. Node05 tells Node02: Tree(Node05) is a heap now. Node02 swaps values with Node05. Node02 tells Node05: Please Reheap Tree(Node05). Node05 swaps values with Node11. Node06 tells Node03: Tree(Node06) is a heap now. Node05 tells Node11: Please Reheap Tree(Node11). You are free to create your own phrasing of the lines that the threads write to standard output. Whatever you do, work it out so that it is very clear. From reading this output, I want to be able to easily tell what happens during the execution of the program, and to get a true impression of the actual order in which events occur. Aim to program the "right" amount of feedback from the threads. It is possible to have too much, and it is also possible to have too little, so use good judgement. Of course, the threads will have to lock standard output while they write their lines of output. ################################################################# Figure out a protocol by which the job indicated can be done. We will have some class discussions to help facilitate this design stage. Once you have decided on the protocol, write it down in pseudo-code form and e-mail it to me by the first due date. This design should be in the form of two or more procedure listings, showing what the threads and the mother process will be doing while the program is running. By the second due date, send a complete, compilable, source listing with adequate comments (The bottom line on the question of documentation is that I need to be able to read your program!) Also, in a separate e-mail message, send a copy of your test script(s). As in the first assignment, do what you can to test for time-dependent errors. Send only the main program, heapify.cpp. I will compile it on a Sun Ultra with my own copies of sem.h and sem.cpp. For this assignment, you may again work in teams of two (2) people. If you work in a team, make sure that both names are in your comments.