SOURCE FILE: worker.cpp
/*
In this program a boss process puts a series of 'work pieces' into
a buffer, one after the other. There are three worker threads
that process each work piece. Worker #i performs task #i.
Each worker needs to have exclusive access to the work piece
while processing it. Each worker must do its task exactly once
on each work piece, but there's no restriction on the order in which
the workers do their processing -- workers 0, 1, and 2 are
permitted to do their tasks in the order 0,1,2 or 0,2,1 or 1,0,2
or 1,2,0 or 2,0,1 or 2,1,0.
Think of each worker as being busy doing lots of other
things much of the time. The program simulates this with random delays.
After all three workers have processed the work piece, the last worker
to process it has to pass it along to a 'Finisher' thread for
final processing.
==============================
The threads implementing the Boss, Workers, and Finisher have to
coordinate and synchronize their actions. Their goals are to
1. avoid using busy waiting to coordinate/synchronize,
2. achieve as much parallel processing as possible,
3. avoid the possibility that a worker could process the same work
piece twice,
4. avoid the possiblity that a worker could process a nonexistent work
piece.
5. avoid the possibility that a worker could overwrite a buffer
before all the processing on its contents has been completed,
6. make sure all the work gets finished, and
7. make sure that the only threads that are involved in
regulating access to a buffer are the threads that either a)
currently are using the buffer, b) currently are trying to get
access to the buffer (in entry code), or c) currently are
releasing the buffer (executing exit code).
Your grade depends on how well you achieve these goals.
*/
#include <iostream>
#include <sched.h>
#include <time.h>
#include <pthread.h>
#include <string>
#include <cmath>
#include "sem.h"
using namespace std ;
/* ######################################## */
/* Global Variables */
/* ######################################## */
const int numStations = 2 ;
const int numWorkers = 3 ;
const int maxJobNames = 10 ;
const int numJobNames = 5 ;
const int WORK = 0 ;
const int FINISH = 1 ;
/* When a buffer[i]==j it means that work piece #j is in buffer[j].*/
/* Dec 18, 2016: Correction of typo above: */
/* When a buffer[i]==j it means that work piece #j is in buffer[i].*/
int buffer[numStations] ;
/* When numTasks_left==k, it means k workers still need to
perform their task on the work piece in buffer[0].
After they do that, the last one gives the work piece
to the Finisher. */
int numTasks_left ;
/* jobName[j] is the name of work piece #j */
string jobName[maxJobNames];
/* child_t are global variables to represent the
dynamically-created threads. */
pthread_t worker_t[numWorkers] ;
pthread_t boss_t ;
/* ######################################## */
/* "Special" Global Variables */
/* ######################################## */
/* Code in sem.cpp "expects" the two variables below to
be here. This particular program does not use
"checking." */
/* "Checking" is just a flag that you set to 1 if you
want lots of debugging messages and set to 0
otherwise. The semaphore code in sem.cpp imports
"checking". Therefore the semaphore operations will
write lots of messages if you set checking=1. */
int checking ;
/* In some programs, we use the "stdoutLock" variable
declared below to get intelligible printouts from
multiple concurrent threads that write to the standard
output. (There has to be something to prevent the
output of the threads from interleaving unintelligibly
on the standard output, and we can't use semaphores if
the semaphore code is writing messages too.)
To print a message to standard output, a thread first
locks standard output, then writes, then unlocks
standard output. See files sem.cpp or conc.cpp for
examples of code that write messages in this manner.
WARNING: DON'T change how the locking of standard
output is done until you've thought a WHOLE lot about
the consequences. In particular, using semaphores to do
the job of stdoutLock can cause "infinite recursion"
under certain circumstances. The reason is that the
semaphore code itself imports "stdoutLock" and writes
messages when the "checking" variable is set to 1. */
pthread_mutex_t stdoutLock ;
/* A data type - a struct (class) with an int field to represent
a thread ID. */
struct threadIdType
{
int id ;
};
/* =========================================================== */
/* In this area, declare whatever semaphores and other
variables you want to use for synchronization */
/* =========================================================== */
/* ################################################## */
/* init */
/* ################################################## */
void init()
{
srandom(time((time_t *) 0)); /* INITIALIZE RANDOM NUMBER GENERATOR */
checking = 0 ;
/* Initialize the "special lock" that is used only to get exclusive
access to the screen. */
if ( 0!=pthread_mutex_init(&stdoutLock, NULL) )
{ cout << "MUTEX INITIALIZATION FAILURE!" << endl; exit(-1) ;}
/* Give some mnemonic names to the work pieces. The first name
is used for an empty buffer. This is coded so no changes are
needed here as long as the value of "numJobNames"
remains the same (10). */
jobName[0]="No Work Piece";
jobName[1]="Work Piece #1" ;
jobName[2]="Work Piece #2" ;
jobName[3]="Work Piece #3" ;
jobName[4]="Work Piece #4" ;
jobName[5]="Work Piece #5" ;
jobName[6]="Work Piece #6" ;
jobName[7]="Work Piece #7" ;
jobName[8]="Work Piece #8" ;
jobName[9]="Work Piece #9" ;
/* =========================================================== */
/* In this area, initialize the semaphores and other
variables you will use for synchronization */
/* =========================================================== */
}
/* ################################################## */
/* DelayAsMuchAs */
/* ################################################## */
void delayAsMuchAs (int limit)
{
int time, step;
time=(int)random()%limit;
// cout << "Time is: " << time << endl ; // to check on randomness
for (step=0;step<time;step++) sched_yield() ;
}
/* ################################################## */
/* Boss */
/* ################################################## */
/* The mother thread spawns a child thread that executes this function.
This function carries out the role of the boss, assigning
work pieces one at a time. */
void * Boss(void * ignore)
{
int i, j, delayLimit=100 ;
for (i=1; i<numJobNames; i++)
{
/* I delay a random time to simulate the time it takes me
to get ready to place another work piece in the buffer.*/
delayAsMuchAs(delayLimit);
/* When the buffer is available,
I place the work piece on the buffer. */
/* THIS IS A PLACE FOR SYNCHRONIZING. */
buffer[WORK] = i; /* put job #i onto buffer[WORK]. */
numTasks_left = numWorkers; /* one task remains for each worker. */
pthread_mutex_lock(&stdoutLock) ;
cout << "Boss places " << jobName[buffer[WORK]]
<< " in buffer #0." << endl ;
pthread_mutex_unlock(&stdoutLock);
/* THIS IS A PLACE FOR SYNCHRONIZING. */
}
pthread_exit ((void *)0) ;
}
/* ################################################## */
/* Worker */
/* ################################################## */
/* The mother thread spawns child threads deignated
as workers. Each of those children executes this
function - with a different id value in the parameter. */
void * Worker(void * idPtr)
{
/* Declare some some local variables (these are not
shared). Type cast the parameter to recover "me"
-- the unique serial number of this particular worker. */
int me = ((threadIdType *) (idPtr))->id ;
int i, j;
/* Try this larger delay to test after adding the
synchronizing code. */
/* int delayLimit=100000 ; */
int delayLimit=100 ;
bool I_pass = false ;
for (i=1; i<numJobNames; i++)
{
/* I delay a random time. This simulates 'other
work' that the worker may have to perform
before getting started on the current work piece.*/
delayAsMuchAs(delayLimit);
/* Here is a diagram of the problem set-up.
Boss
buffer
Worker #1 Worker #2 Worker #3
Finisher
*/
/* THIS IS A PLACE FOR SYNCHRONIZING. */
/* I declare what I am doing */
pthread_mutex_lock(&stdoutLock) ;
cout << "Worker number #" << me << " processes " ;
cout << jobName[buffer[WORK]] << "." << endl ;
pthread_mutex_unlock(&stdoutLock);
/* I delay a random time to simulate the time it takes for me to
perform the work. */
delayAsMuchAs(delayLimit);
numTasks_left-- ;
/* I have to give the work piece to the Finisher if
I am the last worker to process it. */
I_pass = (0==numTasks_left) ;
/* THIS IS A PLACE FOR SYNCHRONIZING. */
/* If I'm supposed to give the work piece to the Finisher */
if (I_pass)
{
/* THIS IS A PLACE FOR SYNCHRONIZING. */
/* Tell the world what I'm doing. */
pthread_mutex_lock(&stdoutLock) ;
cout << "Worker number #"<< me << " gives "
<< jobName[buffer[WORK]] << " to the Finisher." << endl ;
pthread_mutex_unlock(&stdoutLock);
/* transfer the work piece to the next buffer. */
buffer[FINISH]=buffer[WORK] ;
/* make buffer[WORK] empty */
buffer[WORK]=0;
/* THIS IS A PLACE FOR SYNCHRONIZING. */
} /* end of actions performed by the passer */
} /* end of main for-loop */
pthread_exit ((void *)0) ;
} /* end of Worker function */
/* ################################################## */
/* Finisher */
/* ################################################## */
/* The mother thread spawns child threads and then executes this
function. This is convenient because this function should
be the last to exit. This function plays the role of
the Finisher. */
void * Finisher (void * ignore)
{
int i, j, delayLimit=1000 ;
for (i=1; i<numJobNames; i++)
{
/* I delay a random time to simulate whatever work I need
to do before being ready to process another buffer. */
delayAsMuchAs(delayLimit);
/* THIS IS A PLACE FOR SYNCHRONIZING. */
pthread_mutex_lock(&stdoutLock) ;
cout << "Finisher removes "
<< jobName[buffer[FINISH]] << " from buffer #1." << endl ;
pthread_mutex_unlock(&stdoutLock);
buffer[FINISH]=0; // remove the job.
/* THIS IS A PLACE FOR SYNCHRONIZING. */
}
return (void *) 0;
}
/* ################################################## */
/* Main */
/* ################################################## */
int main()
{
init();
cout << endl;
cout << "Welcome to the work site!" << endl ;
cout << endl;
/* This is a pointer to a struct (class) that contains an int
field - it is a convenient data type to use as the parameter
to the child function. */
threadIdType * idPtr ;
int i, ignore=0;
for (i=0; i<numWorkers; i++)
{
idPtr = new threadIdType ; /* allocate memory for struct */
idPtr->id = i ; /* records current index as the child's ID */
/* The call below is what actually creates the child thread
and passes a pointer to the struct 'idPtr' as the
parameter to the child function. */
if (0!=pthread_create(&worker_t[i], NULL, Worker, (void *) idPtr))
{cout << "THREAD CREATION FAILURE!" << endl; exit(-1) ;}
if ( 0!=pthread_detach(worker_t[i] ))
{cout << "THREAD DETACHMENT FAILURE!" << endl ; exit(-1) ;}
}
idPtr = new threadIdType ; /* allocate memory for struct */
idPtr->id = ignore ; /* sets id to 'whatever' */
if (0!=pthread_create(&boss_t, NULL, Boss, (void *) idPtr))
{cout << "THREAD CREATION FAILURE!" << endl; exit(-1) ;}
if (0!=pthread_detach(boss_t))
{cout << "THREAD DETACHMENT FAILURE!" << endl ; exit(-1) ;}
Finisher((void *)0) ;
cout << endl;
cout << "Thank you for your visit!" << endl ;
cout << endl;
return 0 ;
}