------------------------------------------------------------------------------
MC logo
Reader/Writer Test
[^] Process and Thread Synchronization
------------------------------------------------------------------------------
rw3.cpp
#include <stdlib.h>
#include <time.h>

#include <iostream>
#include <algorithm>
#include <string>
#include <list>
#include <algorithm>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#include <utility>

#include "semaphore.h"
#include "readerwriter.h"

using namespace std;

/*
 ******************************************************************************
 * This is a readers/writers test application which maintains a shared linked
 * list of string/integer pairs.  It stars a specified number of reading and
 * writing threads which each perform a sepcified number of operations on
 * the list.  In addition, a monitor thread runs which periodically reports
 * the number of completed operations.  Run with command-line options to
 * control the program's behavior:
 *  -q    Run more quietly.  Want this for most measurements.
 *  -r N  Specify the number of reading threads, default 10.
 *  -w N  Specify the number of writing threads, default 10.
 *  -n N  Specify the number operations (reads or writes) performed by each
 *        thread.
 ******************************************************************************
 */

/*
 * This is a hack to lock cout printing by lines.  It's ugly and not too
 * general, but it seems to work for this.  Use coulock() << ... whatever ...
 * << unlendl; to print a line to cout while holding a mutex so one line won't
 * be interrupted by another.
 */
static recursive_mutex io_mux;
inline ostream &coulock() {
        io_mux.lock();
        return cout;
}
class unlocking_endl_class { };
unlocking_endl_class unlendl;
inline ostream & operator<<(ostream &s, unlocking_endl_class &e)
{
        s << endl;
        io_mux.unlock();
        return s;
}

/*
 * Call rand() under a mutex lock.  Not safe to call from threads unguarded.
 */
int locked_rand()
{
        static mutex mux;
        unique_lock<mutex> locker(mux);
        return rand();
}

/*
 * Choose one of the strings from a list at random.
 */
string rand_name()
{
        string names[] = {
                "international", "consolidated", "confederated", "amalgomated",
                "fubar", "reinitialized", "discombobulated", "missing", 
                "fiduciary", "exasperated", "laminated", "embalmed" };
        int which = locked_rand() % (sizeof names / sizeof names[0]);
        return names[which];
}

/*
 * This is the shared data structure, a un-ordered linked list of names and
 * values.
 */
class data_node {
private:
        int m_value;
        string m_name;
public:
        data_node(string n, int v = 0): m_name(n), m_value(v) { }
        int value() { return m_value; }
        void incval(int i) { m_value += i; }
        string name() { return m_name; }
};
list<data_node> data_list;

/*
 * The reader-writer control object.  This contains the methods for
 * readers and writers to enter and leave the reading and writing sections.
 */
readerwriter rwctl;

/*
 * Find a random name.  If not quiet, prints what it does.
 */
void finder(bool quiet)
{
        // Choose a name at random to look for.
        string tofind = rand_name();

        rwctl.enter_read();

        // Scan for the name.
        auto scan = data_list.begin();
        while(scan != data_list.end()) {
                if(tofind == scan->name()) break;
                ++scan;
        }

        // Print results.
        if(!quiet) {
                if(scan == data_list.end())
                        coulock() << tofind << " is not listed"
                                  << unlendl;
                else
                        coulock() << scan->name() << " is valued at "
                                  << scan->value() << unlendl;
        }
        
        rwctl.leave_read();
}

/*
 * Find the average value in the list.  If not quiet, print it.
 */
void av(bool quiet)
{
        int sum = 0;

        rwctl.enter_read();

        // Go through the list and make the sum.
        auto scan = data_list.begin();
        for(; scan != data_list.end(); ++scan)
                sum += scan->value();

        // Copy the size to local data so we can release.
        int ct = data_list.size();

        rwctl.leave_read();

        // Print results.
        if(!quiet) {
                if(ct > 0)
                        coulock() << "Average is "
                                  << (double)sum / (double)ct
                                  << unlendl;
                else
                        coulock() << "No items to average" << unlendl;
        }
}

/*
 * Find the range.  If not quiet, print the result.
 */
void range(bool quiet)
{
        rwctl.enter_read();

        // Is there anything?
        if(data_list.size() == 0) {
                // No.  Bail.
                rwctl.leave_read();
                if(!quiet)
                        coulock() << "No items for range computation."
                                  << unlendl;
        } else {
                // Yes. Find the range.
                auto scan = data_list.begin();
                int min = scan->value();
                int max = scan->value();
                for(++scan; scan != data_list.end(); ++scan) {
                        if(min > scan->value()) min = scan->value();
                        if(max < scan->value()) max = scan->value();
                }
                rwctl.leave_read();

                // Print the result.
                if(!quiet)
                        coulock() << "Range is " << min << " to "
                                  << max << unlendl;
        }
}

/* This contains a count of reads.  It is only updated occassionally in order
   to reduce overhead.   It is an atomic counter so I can access it from
   different threads without using a mutex. */
atomic<int> read_count(0);

/*
 * Reader thread.  Performs some number of read operations.  Chooses
 * randomly which of finder, av or range to run.
 */
void reader(bool quiet, int cnt)
{
        for(int i = 1; i <= cnt; ++i) {
                // Pick one randomly and run it.
                int choice = locked_rand() % 100;

                if(choice < 20)
                        av(quiet);
                else if(choice < 40)
                        range(quiet);
                else
                        finder(quiet);

                // Sometimes we update the count.
                if(i % 256 == 0) read_count += 256;
        }
        read_count += cnt % 256;
}

/*
 *************** Some updaters. *******************
 */

/*
 * Update a random item.  Find the (an) item given by which, and add
 * the increment.
 */
void update(bool quiet, int increment, string which)
{
        bool created = false;

        rwctl.enter_write();

        // Find in the list. 
        auto scan = data_list.begin();
        for(; scan != data_list.end(); ++scan) {
                if(which == scan->name()) {
                        // If found, increment.
                        scan->incval(increment);
                        break;
                }
        }

        // If not found add to the front.
        if(scan == data_list.end()) {
                // Add to the front of the list.  
                data_list.push_front(data_node(which, increment));
                created = true;
        }

        rwctl.leave_write();

        // Fess up.
        if(!quiet) 
                coulock() << which << " "
                          <<  (created ? "created with" : "given") << " "
                          << increment << unlendl;
}

/*
 * Delete a random item, if present.  If it picks a name which is not in the
 * list, does nothing.
 */
void remove_node(bool quiet)
{
        // Choose a random name.
        string which = rand_name();

        rwctl.enter_write();

        // Scan the list to look for the randomly-chosen name. 
        auto scan = data_list.begin();
        while(scan != data_list.end() && scan->name() != which)
                ++scan;

        // If something was found, remove it. 
        bool was_deleted = false;
        if(scan != data_list.end()) {
                data_list.erase(scan);
                was_deleted = true;
        }

        rwctl.leave_write();

        // Let us know. 
        if(!quiet) {
                if(was_deleted)
                        coulock() << which << " deleted" << unlendl;
                else
                        coulock() << which << " could not be found" << unlendl;
        }
}


/* This contains a count of reads.  It is not always up-to-date */
atomic<int> write_count(0);

/*
 * Update thread.  Performs some number of writing operations.  Chooses
 * randomly which of update or remove_node to call.
 */
void writer(bool quiet, int cnt)
{
        for(int i = 1; i <= cnt; ++i) {
                int choice = locked_rand() % 100;

                if(choice < 65)
                        update(quiet, locked_rand() % 100 - 10, rand_name());
                else
                        remove_node(quiet);

                if(i % 256 == 0) {
                        write_count += 256;
                }
        }
        write_count += cnt % 256;
}

/*
 * This monitors progress. 
 */
atomic<bool> monitor_stop(false);
void monitor(int nread, int nwrite)
{
        while(!monitor_stop) {
                std::this_thread::sleep_for(std::chrono::milliseconds(100));
                int rc = read_count;
                int wc = write_count;
                coulock() << "=== " << rc << " reads (" << 100.0*rc/nread
                          << "%), " << wc << " writes (" << 100.0*wc/nwrite
                          << "%) ===" << unlendl;
        }
}

void usage()
{
        cerr << "Flags: -q[uiet], -r nreader, -w nwriter "
             << "-n numop" << endl;
        exit(1);
}

/*
 * The main starts the threads that start the test threads, then wait it all.
 */
int main(int argc, char **argv)
{
        // Parameters
        bool quiet = false;     // Threads should shut up.
        int nreader = 10;       // Number of reading threads.
        int nwriter = 10;       // Size of update threads.
        int nop = 10;           // Number of operations for each thread.

        // Collect the parameter values.
        for(argc--, argv++; argc > 0; argc--, argv++) {
                if(string(*argv) == "-q") {
                        quiet = true;
                }
                else if((*argv)[0] == '-' && argc > 1) {
                        --argc;
                        switch((*argv++)[1]) {
                        case 'r':
                                nreader = atoi(*argv);
                                break;
                        case 'w':
                                nwriter = atoi(*argv);
                                break;
                        case 'n':
                                nop = atoi(*argv);
                                break;
                        default:
                                usage();
                        }
                } else {
                        usage();
                }
        }

        cout << "Running " << nreader << " read threads and "
             << nwriter << " update threads, " << nop 
             << " operations each" << endl;

        srand(time(NULL));

        // Start the reading and writing threads.  Start them alternately,
        // and collect in a list.
        int totthreads = nreader + nwriter;
        list<thread> threads;
        for(int i = 0; i < max(nreader,nwriter); ++i) {
                if(i < nreader)
                        threads.push_back(thread(reader, quiet, nop));
                if(i < nwriter)
                        threads.push_back(thread(writer, quiet, nop));
        }

        // Start the progress monitor thread.
        thread monthread(monitor, nop*nreader, nop*nwriter);

        // Run join on all the reader/writer threads.
        for(auto & t: threads) {
                t.join();
        }

        // Now that they are done, ask the monitor to stop, then join it.
        monitor_stop = true;
        monthread.join();
}