diff --git a/cpu_scheduling_algorithms/fcfs_scheduling.cpp b/cpu_scheduling_algorithms/fcfs_scheduling.cpp index 5c5e2fa3c58..8a479d780d2 100644 --- a/cpu_scheduling_algorithms/fcfs_scheduling.cpp +++ b/cpu_scheduling_algorithms/fcfs_scheduling.cpp @@ -6,32 +6,42 @@ * arrives first, gets executed first. If two or more processes arrive * simultaneously, the process with smaller process ID gets executed first. * @link https://bit.ly/3ABNXOC - * @author [Pratyush Vatsa](https://github.com/Pratyush219) + * @author [Pratyush Vatsa](https://github.com/Pratyush219) (original + * skeleton) */ #include /// for sorting #include /// for assert -#include -#include /// random number generation -#include /// for time +#include /// for uint32_t #include /// for formatting the output #include /// for IO operations #include /// for std::priority_queue +#include /// for std::mt19937 based random number generation +#include /// for std::tuple #include /// for std::unordered_set #include /// for std::vector -using std::cin; +/** + * @namespace scheduling + * @brief CPU scheduling algorithms + */ +namespace scheduling { +/** + * @namespace fcfs + * @brief Functions for the First Come First Serve algorithm + */ +namespace fcfs { + using std::cout; using std::endl; using std::get; using std::left; using std::make_tuple; using std::priority_queue; -using std::rand; -using std::srand; using std::tuple; using std::unordered_set; using std::vector; + /** * @brief Comparator function for sorting a vector * @tparam S Data type of Process ID @@ -43,7 +53,7 @@ using std::vector; * @returns false if t1 and t2 are in the INCORRECT order */ template -bool sortcol(tuple& t1, tuple& t2) { +bool sortcol(const tuple& t1, const tuple& t2) { if (get<1>(t1) < get<1>(t2)) { return true; } else if (get<1>(t1) == get<1>(t2) && get<0>(t1) < get<0>(t2)) { @@ -73,8 +83,8 @@ class Compare { * @returns true if the tuples SHOULD be swapped * @returns false if the tuples SHOULDN'T be swapped */ - bool operator()(tuple& t1, - tuple& t2) { + bool operator()(const tuple& t1, + const tuple& t2) { // Compare arrival times if (get<1>(t2) < get<1>(t1)) { return true; @@ -189,7 +199,7 @@ class FCFS { * execution * @returns void */ - void printResult() { + void printResult() const { cout << "Status of all the proceses post completion is as follows:" << endl; @@ -214,6 +224,9 @@ class FCFS { /** * @brief Function to be used for testing purposes. This function guarantees the * correct solution for FCFS scheduling algorithm. + * @tparam S Data type of Process ID + * @tparam T Data type of Arrival time + * @tparam E Data type of Burst time * @param input the input data * @details Sorts the input vector according to arrival time. Processes whose * arrival times are same get sorted according to process ID For each process, @@ -224,7 +237,13 @@ class FCFS { */ template vector> get_final_status( - vector> input) { + vector> input) { + // BUG FIX: this used to be hardcoded to + // vector>, which silently discarded + // the S, T, E template parameters for the input type (it only "worked" + // because the self-test happened to instantiate everything as + // uint32_t). It now genuinely matches the class template like the SJF + // file's equivalent function does. sort(input.begin(), input.end(), sortcol); vector> result(input.size()); double timeElapsed = 0; @@ -255,23 +274,31 @@ vector> get_final_status( * @returns void */ static void test() { + // BUG FIX: the original called `srand(time(nullptr))` up to three + // times PER PROCESS, inside a tight loop. `time(nullptr)` only has + // one-second resolution, so many of those reseeds happened within the + // same second and reseeded the generator to the *same* value, + // correlating arrival/burst times instead of randomizing them. Seed a + // proper Mersenne Twister engine exactly once instead. + std::random_device rd; + std::mt19937 eng(rd()); + std::uniform_int_distribution countDist(1, 1000); + std::uniform_int_distribution timeDist(1, 10000); + for (int i{}; i < 1000; i++) { - srand(time(nullptr)); - uint32_t n = 1 + rand() % 1000; + uint32_t n = countDist(eng); FCFS readyQueue; vector> input(n); - for (uint32_t i{}; i < n; i++) { - get<0>(input[i]) = i; - srand(time(nullptr)); - get<1>(input[i]) = 1 + rand() % 10000; - srand(time(nullptr)); - get<2>(input[i]) = 1 + rand() % 10000; + for (uint32_t j{}; j < n; j++) { + get<0>(input[j]) = j; + get<1>(input[j]) = timeDist(eng); + get<2>(input[j]) = timeDist(eng); } - for (uint32_t i{}; i < n; i++) { - readyQueue.addProcess(get<0>(input[i]), get<1>(input[i]), - get<2>(input[i])); + for (uint32_t j{}; j < n; j++) { + readyQueue.addProcess(get<0>(input[j]), get<1>(input[j]), + get<2>(input[j])); } vector> res = get_final_status(input); @@ -281,11 +308,14 @@ static void test() { cout << "All the tests have successfully passed!" << endl; } +} // namespace fcfs +} // namespace scheduling + /** * @brief Entry point of the program * @returns 0 on exit */ int main() { - test(); // run self-test implementations + scheduling::fcfs::test(); // run self-test implementations return 0; -} +} \ No newline at end of file diff --git a/cpu_scheduling_algorithms/non_preemptive_sjf_scheduling.cpp b/cpu_scheduling_algorithms/non_preemptive_sjf_scheduling.cpp index e7d9d370960..60b03bddd79 100644 --- a/cpu_scheduling_algorithms/non_preemptive_sjf_scheduling.cpp +++ b/cpu_scheduling_algorithms/non_preemptive_sjf_scheduling.cpp @@ -1,26 +1,47 @@ /** * @file - * @brief Implementation of SJF CPU scheduling algorithm + * @brief Implementation of the non-preemptive Shortest Job First (SJF) CPU + * scheduling algorithm * @details - * shortest job first (SJF), also known as shortest job next (SJN), is a - * scheduling policy that selects for execution the waiting process with the - * smallest execution time. SJN is a non-preemptive algorithm. Shortest - * remaining time is a preemptive variant of SJN. + * Shortest job first (SJF), also known as shortest job next (SJN), is a + * scheduling policy that selects for execution the *already arrived* + * process with the smallest burst (execution) time. This is the + * non-preemptive variant: once a process starts running it is not + * interrupted, even if a shorter job arrives while it is executing. * * detailed description on SJF scheduling - * Author : Lakshmi Srikumar + * + * A correct implementation must never schedule a process that has not yet + * arrived, even if it has the smallest burst time of all the processes + * submitted to the scheduler. At every decision point, only processes + * that have arrived by the current simulated time are eligible to run; if + * none have arrived, the CPU sits idle until the next arrival. + * + * @author [Lakshmi Srikumar](https://github.com/LakshmiSrikumar) (original + * skeleton) */ -#include /// for sorting +#include /// for std::sort #include /// for assert #include /// for formatting the output #include /// for IO operations #include /// for std::priority_queue -#include /// random number generation +#include /// for std::mt19937 based random number generation +#include /// for std::tuple #include /// for std::unordered_set #include /// for std::vector -using std::cin; +/** + * @namespace scheduling + * @brief CPU scheduling algorithms + */ +namespace scheduling { +/** + * @namespace non_preemptive_sjf + * @brief Functions for the non-preemptive Shortest Job First algorithm + */ +namespace non_preemptive_sjf { + using std::cout; using std::endl; using std::get; @@ -32,177 +53,174 @@ using std::unordered_set; using std::vector; /** - * @brief Comparator function for sorting a vector - * @tparam S Data type of Process ID - * @tparam T Data type of Arrival time - * @tparam E Data type of Burst time - * @param t1 First tuplet1 - * @param t2 Second tuplet2 - * @returns true if t1 and t2 are in the CORRECT order - * @returns false if t1 and t2 are in the INCORRECT order + * Result tuple layout, used throughout this file: + * 1st element: Process ID + * 2nd element: Arrival Time + * 3rd element: Burst time + * 4th element: Completion time + * 5th element: Turnaround time + * 6th element: Waiting time + */ +template +using process_result_t = tuple; + +/** + * @brief Orders raw (id, arrival, burst) tuples by arrival time, tie-broken + * by process ID. + * @details This is used purely to determine the ORDER in which processes + * become available to the scheduler. It does not decide execution order. */ template -bool sortcol(tuple& t1, tuple& t2) { - if (get<1>(t1) < get<1>(t2) || - (get<1>(t1) == get<1>(t2) && get<0>(t1) < get<0>(t2))) { - return true; +struct ArrivalOrder { + bool operator()(const tuple& a, const tuple& b) const { + if (get<1>(a) != get<1>(b)) { + return get<1>(a) < get<1>(b); + } + return get<0>(a) < get<0>(b); } - return false; -} +}; /** - * @class Compare - * @brief Comparator class for priority queue - * @tparam S Data type of Process ID - * @tparam T Data type of Arrival time - * @tparam E Data type of Burst time + * @brief Min-heap comparator for the "ready queue" of already-arrived + * processes: smallest burst time first, ties broken by earliest arrival, + * then smallest process ID. + * @details std::priority_queue is a max-heap with respect to the supplied + * comparator, so to make the *smallest* burst time appear at top() this + * comparator must report the *larger*-burst tuple as having lower priority. */ template -class Compare { - public: - /** - * @param t1 First tuple - * @param t2 Second tuple - * @brief A comparator function that checks whether to swap the two tuples - * or not. - * - * detailed description of comparator - * @returns true if the tuples SHOULD be swapped - * @returns false if the tuples SHOULDN'T be swapped - */ - bool operator()(tuple& t1, - tuple& t2) { - // Compare burst times for SJF - if (get<2>(t2) < get<2>(t1)) { - return true; +struct ReadyQueueOrder { + bool operator()(const process_result_t& a, + const process_result_t& b) const { + if (get<2>(a) != get<2>(b)) { + return get<2>(a) > get<2>(b); } - // If burst times are the same, compare arrival times - else if (get<2>(t2) == get<2>(t1)) { - return get<1>(t2) < get<1>(t1); + if (get<1>(a) != get<1>(b)) { + return get<1>(a) > get<1>(b); } - return false; + return get<0>(a) > get<0>(b); } }; /** * @class SJF - * @brief Class which implements the SJF scheduling algorithm + * @brief Class which implements the non-preemptive SJF scheduling algorithm * @tparam S Data type of Process ID * @tparam T Data type of Arrival time * @tparam E Data type of Burst time */ template class SJF { - /** - * Priority queue of schedules(stored as tuples) of processes. - * In each tuple - * @tparam 1st element: Process ID - * @tparam 2nd element: Arrival Time - * @tparam 3rd element: Burst time - * @tparam 4th element: Completion time - * @tparam 5th element: Turnaround time - * @tparam 6th element: Waiting time - */ - priority_queue, - vector>, - Compare> - schedule; + /// Raw (id, arrival, burst) triples exactly as submitted. + vector> processes{}; - // Stores final status of all the processes after completing the execution. - vector> result; + /// Final scheduled results, in the order processes were executed. + vector> result{}; - // Stores process IDs. Used for confirming absence of a process while it. - unordered_set idList; + /// Tracks process IDs already added, to reject duplicates. + unordered_set idList{}; public: /** - * @brief Adds the process to the ready queue if it isn't already there + * @brief Registers a process with the scheduler if its ID hasn't been + * used already. * @param id Process ID * @param arrival Arrival time of the process * @param burst Burst time of the process * @returns void - * */ void addProcess(S id, T arrival, E burst) { - // Add if a process with process ID as id is not found in idList. if (idList.find(id) == idList.end()) { - tuple t = - make_tuple(id, arrival, burst, 0, 0, 0); - schedule.push(t); + processes.emplace_back(id, arrival, burst); idList.insert(id); } } /** - * @brief Algorithm for scheduling CPU processes according to - * the Shortest Job First (SJF) scheduling algorithm. - * - * @details Non pre-emptive SJF is an algorithm that schedules processes - * based on the length of their burst times. The process with the smallest - * burst time is executed first.In a non-preemptive scheduling algorithm, - * once a process starts executing,it runs to completion without being - * interrupted. - * - * I used a min priority queue because it allows you to efficiently pick the - * process with the smallest burst time in constant time, by maintaining a - * priority order where the shortest burst process is always at the front. + * @brief Runs non-preemptive SJF scheduling over every registered + * process. + * @details We keep two collections: + * - `byArrival`: every process, sorted by arrival time, that has NOT + * yet been admitted to the ready queue. + * - `ready`: a min-heap (by burst time) of every process that HAS + * arrived but not yet run. * - * @returns void + * At each step we admit every process whose arrival time is at most + * the current simulated time into `ready`. If `ready` is still empty + * (nothing has arrived), the CPU is idle, so we fast-forward the clock + * to the next arrival. Otherwise we run whichever admitted process has + * the smallest burst time. This guarantees a process can never be + * scheduled before it arrives, regardless of how short its burst is. + * @returns Results in execution order. */ + vector> scheduleForSJF() { + result.clear(); + if (processes.empty()) { + return result; + } - vector> scheduleForSJF() { - // Variable to keep track of time elapsed so far - double timeElapsed = 0; + vector> byArrival = processes; + std::sort(byArrival.begin(), byArrival.end(), ArrivalOrder()); - while (!schedule.empty()) { - tuple cur = schedule.top(); + priority_queue, + vector>, + ReadyQueueOrder> + ready; - // If the current process arrived at time t2, the last process - // completed its execution at time t1, and t2 > t1. - if (get<1>(cur) > timeElapsed) { - timeElapsed += get<1>(cur) - timeElapsed; - } + const size_t n = byArrival.size(); + size_t nextToArrive = 0; + double timeElapsed = 0; - // Add Burst time to time elapsed - timeElapsed += get<2>(cur); + while (result.size() < n) { + // Admit every process that has arrived by now. + while (nextToArrive < n && + get<1>(byArrival[nextToArrive]) <= timeElapsed) { + const auto& p = byArrival[nextToArrive]; + ready.push( + make_tuple(get<0>(p), get<1>(p), get<2>(p), 0.0, 0.0, 0.0)); + ++nextToArrive; + } - // Completion time of the current process will be same as time - // elapsed so far - get<3>(cur) = timeElapsed; + // CPU is idle: nothing has arrived yet, so jump the clock + // forward to the next arrival instead of guessing. + if (ready.empty()) { + timeElapsed = get<1>(byArrival[nextToArrive]); + continue; + } - // Turnaround time = Completion time - Arrival time - get<4>(cur) = get<3>(cur) - get<1>(cur); + // Run the shortest job among those that have already arrived. + process_result_t cur = ready.top(); + ready.pop(); - // Waiting time = Turnaround time - Burst time - get<5>(cur) = get<4>(cur) - get<2>(cur); + timeElapsed += get<2>(cur); // Add burst time + get<3>(cur) = timeElapsed; // Completion time + get<4>(cur) = get<3>(cur) - get<1>(cur); // Turnaround time + get<5>(cur) = get<4>(cur) - get<2>(cur); // Waiting time // Turnaround time >= Burst time assert(get<4>(cur) >= get<2>(cur)); - // Waiting time is never negative assert(get<5>(cur) >= 0); result.push_back(cur); - schedule.pop(); } + return result; } + /** - * @brief Utility function for printing the status of - * each process after execution + * @brief Utility function for printing the status of each process + * @param toPrint the processes to print * @returns void */ - - void printResult( - const vector>& processes) { + void printResult(const vector>& toPrint) const { cout << std::setw(17) << left << "Process ID" << std::setw(17) << left << "Arrival Time" << std::setw(17) << left << "Burst Time" << std::setw(17) << left << "Completion Time" << std::setw(17) << left << "Turnaround Time" << std::setw(17) << left << "Waiting Time" << endl; - for (const auto& process : processes) { + for (const auto& process : toPrint) { cout << std::setprecision(2) << std::fixed << std::setw(17) << left << get<0>(process) << std::setw(17) << left << get<1>(process) << std::setw(17) << left << get<2>(process) << std::setw(17) @@ -214,49 +232,69 @@ class SJF { }; /** - * @brief Computes the final status of processes after - * applying non-preemptive SJF scheduling + * @brief Independent O(n^2) reference implementation, used only to check + * SJF::scheduleForSJF() against in tests. + * @details At every scheduling step, scans every not-yet-completed process, + * considers only the ones that have already arrived by the current + * simulated time, and greedily picks the smallest burst time (ties broken + * by arrival, then ID) -- deliberately written differently from the + * priority-queue approach above so the two can catch each other's bugs. * @tparam S Data type of Process ID * @tparam T Data type of Arrival time * @tparam E Data type of Burst time - * @param input A vector of tuples containing Process ID, Arrival time, and - * Burst time - * @returns A vector of tuples containing Process ID, Arrival time, Burst time, - * Completion time, Turnaround time, and Waiting time + * @param input A vector of (id, arrival, burst) tuples + * @returns Results in execution order. */ template -vector> get_final_status( +vector> get_final_status( vector> input) { - // Sort the processes based on Arrival time and then Burst time - sort(input.begin(), input.end(), sortcol); + const size_t n = input.size(); + vector done(n, false); + vector> result; + result.reserve(n); - // Result vector to hold the final status of each process - vector> result(input.size()); double timeElapsed = 0; - for (size_t i = 0; i < input.size(); i++) { - // Extract Arrival time and Burst time - T arrival = get<1>(input[i]); - E burst = get<2>(input[i]); - - // If the CPU is idle, move time to the arrival of the next process - if (arrival > timeElapsed) { - timeElapsed = arrival; + for (size_t scheduled = 0; scheduled < n; ++scheduled) { + int best = -1; + for (size_t i = 0; i < n; ++i) { + if (done[i] || get<1>(input[i]) > timeElapsed) { + continue; + } + if (best == -1 || get<2>(input[i]) < get<2>(input[best]) || + (get<2>(input[i]) == get<2>(input[best]) && + get<1>(input[i]) < get<1>(input[best])) || + (get<2>(input[i]) == get<2>(input[best]) && + get<1>(input[i]) == get<1>(input[best]) && + get<0>(input[i]) < get<0>(input[best]))) { + best = static_cast(i); + } } - // Update timeElapsed by adding the burst time - timeElapsed += burst; + if (best == -1) { + // Nothing eligible has arrived yet; jump to the earliest + // remaining arrival and retry this same slot. + double nextArrival = -1; + for (size_t i = 0; i < n; ++i) { + if (!done[i] && + (nextArrival < 0 || get<1>(input[i]) < nextArrival)) { + nextArrival = get<1>(input[i]); + } + } + timeElapsed = nextArrival; + --scheduled; + continue; + } - // Calculate Completion time, Turnaround time, and Waiting time + done[best] = true; + timeElapsed += get<2>(input[best]); double completion = timeElapsed; - double turnaround = completion - arrival; - double waiting = turnaround - burst; - - // Store the results in the result vector - result[i] = make_tuple(get<0>(input[i]), arrival, burst, completion, - turnaround, waiting); + double turnaround = completion - get<1>(input[best]); + double waiting = turnaround - get<2>(input[best]); + result.push_back(make_tuple(get<0>(input[best]), get<1>(input[best]), + get<2>(input[best]), completion, turnaround, + waiting)); } - return result; } @@ -265,52 +303,59 @@ vector> get_final_status( * @returns void */ static void test() { - // A vector to store the results of all processes across all test cases. - vector> - finalResult; - - for (int i{}; i < 10; i++) { - std::random_device rd; // Seeding - std::mt19937 eng(rd()); - std::uniform_int_distribution<> distr(1, 10); - - uint32_t n = distr(eng); - SJF readyQueue; - vector> - input(n); - - // Generate random arrival and burst times - for (uint32_t i{}; i < n; i++) { - get<0>(input[i]) = i; - get<1>(input[i]) = distr(eng); // Random arrival time - get<2>(input[i]) = distr(eng); // Random burst time - } + // Regression case: this is the exact scenario the previous + // priority-queue-by-burst-only implementation got wrong. Process A + // arrives first with the CPU idle and must run immediately, even + // though B (arriving later) has a shorter burst time. + { + SJF q; + q.addProcess(0, 0, 10); // A: arrives at 0, burst 10 + q.addProcess(1, 1, 1); // B: arrives at 1, burst 1 + + auto res = q.scheduleForSJF(); + + assert(get<0>(res[0]) == 0); // A must run first + assert(get<3>(res[0]) == 10); // A completes at t=10 + assert(get<5>(res[0]) == 0); // A never waits + + assert(get<0>(res[1]) == 1); // B runs second + assert(get<3>(res[1]) == 11); // B completes at t=11 + assert(get<5>(res[1]) == 9); // B waits 9 units for the CPU + } + + // Randomized cross-check against the independent O(n^2) oracle. + std::random_device rd; + std::mt19937 eng(rd()); + std::uniform_int_distribution countDist(1, 12); + std::uniform_int_distribution timeDist(1, 20); - // Print processes before scheduling - cout << "Processes before SJF scheduling:" << endl; - readyQueue.printResult(input); + for (int trial = 0; trial < 500; ++trial) { + int n = countDist(eng); + SJF readyQueue; + vector> input(n); - // Add processes to the queue - for (uint32_t i{}; i < n; i++) { + for (int i = 0; i < n; ++i) { + input[i] = make_tuple(i, timeDist(eng), timeDist(eng)); readyQueue.addProcess(get<0>(input[i]), get<1>(input[i]), get<2>(input[i])); } - // Perform SJF schedulings - auto finalResult = readyQueue.scheduleForSJF(); - - // Print processes after scheduling - cout << "\nProcesses after SJF scheduling:" << endl; - readyQueue.printResult(finalResult); + auto expected = get_final_status(input); + auto actual = readyQueue.scheduleForSJF(); + assert(expected == actual); } + cout << "All the tests have successfully passed!" << endl; } +} // namespace non_preemptive_sjf +} // namespace scheduling + /** * @brief Main function * @returns 0 on successful exit */ int main() { - test(); + scheduling::non_preemptive_sjf::test(); return 0; -} +} \ No newline at end of file diff --git a/cpu_scheduling_algorithms/round_robin_scheduling.cpp b/cpu_scheduling_algorithms/round_robin_scheduling.cpp new file mode 100644 index 00000000000..4a42c3e289d --- /dev/null +++ b/cpu_scheduling_algorithms/round_robin_scheduling.cpp @@ -0,0 +1,398 @@ +/** + * @file + * @brief Implementation of the Round Robin (RR) CPU scheduling algorithm + * @details + * Round Robin is a preemptive CPU scheduling algorithm. Every process in + * the ready queue is given a fixed unit of CPU time called a "time + * quantum". If a process does not finish within its quantum it is + * preempted and placed at the back of the ready queue; processes that + * arrive while another process is running are admitted to the ready + * queue before the preempted process is placed back at the end (the + * standard convention used by most textbooks and OS courses). + * + * detailed description on Round Robin scheduling + * + * @author [Round Robin skeleton written for this conversation] + */ + +#include /// for std::sort +#include /// for assert +#include /// for uint32_t +#include /// for std::deque (the ready queue) +#include /// for formatting the output +#include /// for IO operations +#include /// for std::mt19937 based random number generation +#include /// for std::tuple +#include /// for std::unordered_set +#include /// for std::vector + + /** + * @namespace scheduling + * @brief CPU scheduling algorithms + */ +namespace scheduling { + /** + * @namespace round_robin + * @brief Functions for the Round Robin algorithm + */ + namespace round_robin { + + using std::cout; + using std::deque; + using std::endl; + using std::get; + using std::left; + using std::make_tuple; + using std::tuple; + using std::unordered_set; + using std::vector; + + /** + * Result tuple layout, used throughout this file: + * 1st element: Process ID + * 2nd element: Arrival Time + * 3rd element: Burst time (the ORIGINAL, total burst time -- not the + * time remaining at any intermediate point) + * 4th element: Completion time + * 5th element: Turnaround time + * 6th element: Waiting time + */ + template + using process_result_t = tuple; + + /** + * @brief Orders raw (id, arrival, burst) tuples by arrival time, tie-broken + * by process ID. + * @details This single ordering is used everywhere admission decisions are + * made (both in the scheduler and in the independent test oracle below) so + * that simultaneous arrivals are always admitted in the same, consistent + * order. + */ + template + struct ArrivalOrder { + bool operator()(const tuple& a, const tuple& b) const { + if (get<1>(a) != get<1>(b)) { + return get<1>(a) < get<1>(b); + } + return get<0>(a) < get<0>(b); + } + }; + + /** + * @class RoundRobin + * @brief Class which implements the Round Robin scheduling algorithm + * @tparam S Data type of Process ID + * @tparam T Data type of Arrival time + * @tparam E Data type of Burst time + */ + template + class RoundRobin { + /// Raw (id, arrival, burst) triples exactly as submitted. + vector> processes{}; + + /// Tracks process IDs already added, to reject duplicates. + unordered_set idList{}; + + public: + /** + * @brief Registers a process with the scheduler if its ID hasn't been + * used already. + * @param id Process ID + * @param arrival Arrival time of the process + * @param burst Burst time of the process + * @returns void + */ + void addProcess(S id, T arrival, E burst) { + if (idList.find(id) == idList.end()) { + processes.emplace_back(id, arrival, burst); + idList.insert(id); + } + } + + /** + * @brief Runs Round Robin scheduling with the given time quantum over + * every registered process. + * @details Processes are admitted to a FIFO ready queue strictly in + * arrival order (ties broken by process ID). Whenever the running + * process is preempted (its quantum expires before it finishes), any + * process that arrived during that quantum is enqueued first, and the + * preempted process is placed at the back of the queue afterwards -- + * this is what makes the scheduler "fair" instead of letting a + * process that keeps getting preempted immediately cut back in line + * ahead of everyone who arrived while it was running. + * @param quantum The fixed time slice given to each process per turn. + * Must be strictly greater than zero. + * @returns Results in the order processes finished executing. + */ + vector> scheduleForRR(E quantum) { + assert(quantum > 0); + + vector> result; + if (processes.empty()) { + return result; + } + + vector> byArrival = processes; + std::sort(byArrival.begin(), byArrival.end(), ArrivalOrder()); + + const size_t n = byArrival.size(); + vector remaining(n); + for (size_t i = 0; i < n; ++i) { + remaining[i] = get<2>(byArrival[i]); + } + + deque ready; + size_t nextToArrive = 0; + double currentTime = 0; + size_t completedCount = 0; + + // Admits every not-yet-admitted process whose arrival time is at + // most `currentTime`, in (arrival, id) order. + auto admitArrived = [&]() { + while (nextToArrive < n && + get<1>(byArrival[nextToArrive]) <= currentTime) { + ready.push_back(nextToArrive); + ++nextToArrive; + } + }; + + admitArrived(); + + while (completedCount < n) { + if (ready.empty()) { + // CPU idle: nothing has arrived yet, jump the clock + // forward to the next arrival instead of guessing. + assert(nextToArrive < n); + currentTime = get<1>(byArrival[nextToArrive]); + admitArrived(); + continue; + } + + size_t idx = ready.front(); + ready.pop_front(); + + E execTime = remaining[idx] < quantum ? remaining[idx] : quantum; + currentTime += execTime; + remaining[idx] -= execTime; + + // Anyone who arrived during this time slice is admitted + // BEFORE the process we just ran gets a chance to re-queue. + admitArrived(); + + if (remaining[idx] == 0) { + const auto& p = byArrival[idx]; + double completion = currentTime; + double turnaround = completion - get<1>(p); + double waiting = turnaround - get<2>(p); + + assert(turnaround >= get<2>(p)); // turnaround >= burst + assert(waiting >= 0); // waiting is never negative + + result.push_back(make_tuple(get<0>(p), get<1>(p), get<2>(p), + completion, turnaround, waiting)); + ++completedCount; + } + else { + ready.push_back(idx); + } + } + + return result; + } + + /** + * @brief Utility function for printing the status of each process + * @param toPrint the processes to print + * @returns void + */ + void printResult(const vector>& toPrint) const { + cout << std::setw(17) << left << "Process ID" << std::setw(17) << left + << "Arrival Time" << std::setw(17) << left << "Burst Time" + << std::setw(17) << left << "Completion Time" << std::setw(17) + << left << "Turnaround Time" << std::setw(17) << left + << "Waiting Time" << endl; + + for (const auto& process : toPrint) { + cout << std::setprecision(2) << std::fixed << std::setw(17) + << left << get<0>(process) << std::setw(17) << left + << get<1>(process) << std::setw(17) << left + << get<2>(process) << std::setw(17) << left + << get<3>(process) << std::setw(17) << left + << get<4>(process) << std::setw(17) << left + << get<5>(process) << endl; + } + } + }; + + /** + * @brief Independent unit-tick reference implementation, used only to + * check RoundRobin::scheduleForRR() against in tests. + * @details Instead of jumping the clock forward by a whole quantum at a + * time, this oracle advances time one unit at a time and re-evaluates + * arrivals and quantum expiry after every single tick. It is far less + * efficient, but its correctness is easy to see by inspection, which + * makes it a trustworthy check for the optimized implementation above. + * @tparam S Data type of Process ID + * @tparam T Data type of Arrival time + * @tparam E Data type of Burst time + * @param input A vector of (id, arrival, burst) tuples + * @param quantum The fixed time slice given to each process per turn + * @returns Results in the order processes finished executing. + */ + template + vector> get_final_status( + vector> input, E quantum) { + vector> byArrival = input; + std::sort(byArrival.begin(), byArrival.end(), ArrivalOrder()); + + const size_t n = byArrival.size(); + vector remaining(n); + for (size_t i = 0; i < n; ++i) { + remaining[i] = get<2>(byArrival[i]); + } + + vector> result; + if (n == 0) { + return result; + } + + deque ready; + size_t nextToArrive = 0; + double time = 0; + size_t completedCount = 0; + int running = -1; + E ticksUsed = 0; + + auto admitArrived = [&]() { + while (nextToArrive < n && get<1>(byArrival[nextToArrive]) <= time) { + ready.push_back(nextToArrive); + ++nextToArrive; + } + }; + + admitArrived(); + + while (completedCount < n) { + if (running == -1) { + if (ready.empty()) { + // CPU idle for this tick. + ++time; + admitArrived(); + continue; + } + running = static_cast(ready.front()); + ready.pop_front(); + ticksUsed = 0; + } + + // Execute one tick of the running process. + --remaining[running]; + ++ticksUsed; + ++time; + + admitArrived(); + + if (remaining[running] == 0) { + const auto& p = byArrival[running]; + double completion = time; + double turnaround = completion - get<1>(p); + double waiting = turnaround - get<2>(p); + result.push_back(make_tuple(get<0>(p), get<1>(p), get<2>(p), + completion, turnaround, waiting)); + ++completedCount; + running = -1; + } + else if (ticksUsed == quantum) { + ready.push_back(static_cast(running)); + running = -1; + } + } + + return result; + } + + /** + * @brief Self-test implementations + * @returns void + */ + static void test() { + // Regression case: classic textbook example. + // P0 arrives at 0 with burst 5, P1 arrives at 1 with burst 3, + // P2 arrives at 2 with burst 1, quantum = 2. + // Expected trace (well-known result for this exact example): + // 0-2: P0 (rem 3) | ready after: P1, P2, P0 + // 2-4: P1 (rem 1) | ready after: P2, P0, P1 + // 4-5: P2 (rem 0) -> done at t=5 + // 5-7: P0 (rem 1) | ready after: P1, P0 + // 7-8: P1 (rem 0) -> done at t=8 + // 8-9: P0 (rem 0) -> done at t=9 + { + RoundRobin rr; + rr.addProcess(0, 0, 5); + rr.addProcess(1, 1, 3); + rr.addProcess(2, 2, 1); + auto res = rr.scheduleForRR(2); + + assert(res.size() == 3); + // Completion order: P2, then P1, then P0. + assert(get<0>(res[0]) == 2); + assert(get<3>(res[0]) == 5); + + assert(get<0>(res[1]) == 1); + assert(get<3>(res[1]) == 8); + + assert(get<0>(res[2]) == 0); + assert(get<3>(res[2]) == 9); + } + + // A process that never gets preempted (burst <= quantum) should + // behave exactly like FCFS with respect to that single process. + { + RoundRobin rr; + rr.addProcess(0, 0, 4); + auto res = rr.scheduleForRR(10); + assert(res.size() == 1); + assert(get<3>(res[0]) == 4); + assert(get<5>(res[0]) == 0); // no waiting, nothing else queued + } + + // Randomized cross-check against the independent unit-tick oracle, + // across a range of quanta. + std::random_device rd; + std::mt19937 eng(rd()); + std::uniform_int_distribution countDist(1, 10); + std::uniform_int_distribution timeDist(1, 15); + std::uniform_int_distribution quantumDist(1, 6); + + for (int trial = 0; trial < 500; ++trial) { + int n = countDist(eng); + int quantum = quantumDist(eng); + + RoundRobin rr; + vector> input(n); + + for (int i = 0; i < n; ++i) { + input[i] = make_tuple(i, timeDist(eng), timeDist(eng)); + rr.addProcess(get<0>(input[i]), get<1>(input[i]), + get<2>(input[i])); + } + + auto expected = get_final_status(input, quantum); + auto actual = rr.scheduleForRR(quantum); + assert(expected == actual); + } + + cout << "All the tests have successfully passed!" << endl; + } + + } // namespace round_robin +} // namespace scheduling + +/** + * @brief Main function + * @returns 0 on successful exit + */ +int main() { + scheduling::round_robin::test(); + return 0; +} \ No newline at end of file