In this assignment, you will use the producer-consumer solution we discussed in class to manage access to a bounded buffer storing candy. One group of threads will model candy factories which generate candy one at a time and insert the candy into the bounded buffer. Another group of threads will model kids which eat candy one a time from the bounded buffer. Your program, called candykids
, will accept three arguments:
./candykids <#factories> <#kids> <#seconds>
Example: ./candykids 3 1 10
# Factories: Number of candy-factory threads to spawn.
# Kids: Number of kid threads to spawn.
# Seconds: Number of seconds to allow the factory threads to run for.
Your main()
function will start and control the application. Its steps are as follows:
main() {
// 1. Extract arguments
// 2. Initialize modules
// 3. Launch candy-factory threads
// 4. Launch kid threads
// 5. Wait for requested time
// 6. Stop candy-factory threads
// 7. Wait until no more candy
// 8. Stop kid threads
// 9. Print statistics
// 10. Cleanup any allocated memory
}
sleep(1)
. Loop as many times as the “# Seconds” command line argument. Print the number of seconds running each time, such as “Time 3s” after the 3rd sleep. This shows time ticking away as your program executes.Waiting for all candy to be consumed
” and sleep for 1 second.Stop kid threads
For each kid thread, cancel the thread and then join the thread. For example, if a thread ID is stored in daThreadID
, you would run:
```cpp
pthread_cancel(daThreadId); pthread_join(daThreadId, NULL); ```
You must split your code up into modules by using multiple .h and .c files.
Suggestion is to have the following files:
candykids.c
: Main application holding factory thread, kid thread, and main() function. Plus some other helper functions, and some #defined constants.bbuff.h/.c
: Bounded buffer module (see below).stats.h/stats.c
: Statistics module (see later section).Makefile
: Must compile all the .c files and link together the .o files.The factory creates candy and the kids consume it. The candy will be stored in a bounded buffer. To do this, you need a data type to represent the candy. The following struct
is convenient:
typedef struct {
int factory_number;
double time_stamp_in_ms;
} candy_t;
factory_number
tracks which factory thread produced the candy item.time_stamp_in_ms
tracks when the item was created. You can get the current number of milliseconds using the following function. This code must be linked with the -lrt flag. Add it to CFLAGS in your Makefile.2
double current_time_in_ms(void)
{
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
return now.tv_sec * 1000.0 + now.tv_nsec/1000000.0;
}
Create a bounded buffer module which encapsulates access to the bounded buffer. Your bounded buffer must be implemented using the producer-consumer technique we discussed in clasa.
Suggested public interface (the complete bbuff.h
file) is shown below. Note that it operates on void*
pointers instead of directly with candy_t
structures. This is done so that the buffer need not know anything about the type of information it is storing. In this case, make the buffer array (declared in the .c file) of type void*
such as: void* da_data[DA_SIZE]
;
#ifndef BBUFF_H
#define BBUFF_H
#define BUFFER_SIZE 10
void bbuff_init(void);
void bbuff_blocking_insert(void* item);
void* bbuff_blocking_extract(void);
_Bool bbuff_is_empty(void);
#endif
For example, an item can be inserted into the buffer with the following code which will dynamically allocate one candy element (pointer stored in candy
), set the fields of the candy, and then call the bounded buffer function to insert it into the bounded buffer.
void foo() {
candy_t *candy = malloc(...);
candy->factory_number = ...;
candy->time_stamp_in_ms = ...;
bbuff_blocking_insert(candy);
}
The bbuff_init()
function is used to initialize the bounded buffer module if anything needs to be initialized. Think of it like the constructor for your module: if this were object-oriented C++ then the constructor would do any needed initialization. But, in C there are no constructors, so init()
functions are often used. If you do not need to initialize anything in your module, you can omit the bbuff_init()
function entirely.
Each candy-factory thread should:
main()
signals to exit (see below)
"\tFactory 0 ships candy & waits 2s"
"Candy-factory 0 done"
The thread will end when signaled to do so by main()
. This is not using Linux signals but rather just a _Bool
global variable which is set to true when it’s time to end the thread. For example, name it stop_thread
and have it be false
to start. Then have main()
, when it wants to end the thread, set this variable to true
. Have your thread continually check this _Bool
variable (often called a flag) to see if it should end. Here is some pseudo-code that may help:
_Bool stop_thread = false;
void* dathread_function(void* arg) {
while (!stop_thread) {
// Do the work of the thread
}
printf("Done!");
}
void main() {
// Spawn thread
pthread_id daThreadId;
pthread_create(&daThreadId, ...)
// Wait
sleep(...)
// Tell thread to stop itself, and then wait until it's done.
stop_thread = true;
pthread_join(daThreadID, NULL)
}
Each kid thread should do the following:
printf()
it to the screen; in the next section, you must add a statistics module that will track what candies have been eaten.main()
using pthread_cancel()
. When this occurs, it is likely that the kid thread will be waiting on the semaphore in the bounded buffer. This should not cause problems.Create a statistics module tracking:
stats.h
):#ifndef STATS_H
#define STATS_H
void stats_init(int num_producers);
void stats_cleanup(void);
void stats_record_produced(int factory_number);
void stats_record_consumed(int factory_number, double delay_in_ms);
void stats_display(void);
#endif
Internally in stats.c
, you will likely need to track a number of values for each candy-factory. It is suggested you create a struct
with all required fields, and then build an array of such structs
(one element for each candy-factory).
The stats_init()
function can initialize your data storage and get it ready to process produced and consumed events (via the respective functions). The stats_cleanup()
function is used to free any dynamically allocated memory. This function should be called just before main()
terminates.
When the program ends, you must display a table summarizing the statistics gathered by the program. For example, it should resemble quite closely:
Statistics:
Factory# #Made #Eaten Min Delay[ms] Avg Delay[ms] Max Delay[ms]
0 5 5 0.60498 2602.81274 5004.28369
1 5 5 0.40454 2202.97290 5005.06494
2 7 7 0.60107 2287.86067 4004.16162
3 8 8 1001.12012 2377.36115 5004.13159
4 5 5 0.40186 2202.63008 5005.38330
5 4 4 1003.22095 2503.94049 4006.16309
6 5 5 1003.24487 2603.35894 4005.19873
7 6 6 3002.30640 3836.61743 4005.16089
8 4 4 3001.74048 3753.03259 5004.31177
9 4 4 3002.76660 4253.44440 5005.13550
printf("%8s%10s%10s\n", "First", "Second", "Third");
printf("%8d%10.5f%10.5f\n", 1, 2.123456789, 3.14157932523);
valgrind
will be used to check for memory leaks. Don’t worry if valgrind
reports “still accessible” memory which was allocated from any function called from pthread_exit();
you may get a few such warnings. But all memory that you allocate must be freed and not be”still accessible”.
You can run valgrind
with the following command:
valgrind --leak-check=full --show-leak-kinds=all --num-callers=20 ./candykids 8 1 1
See here for some sample outputs.
Submit an archive (.tar.gz) to CourSys of your code and Makefile
. We will build your code using your Makefile
, and then run it using a command like: ./candykids 2 2 10
Please remember that all submissions will automatically be compared for unexplainable similarities.