C++ Readers/Writers
#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(); }