Multithreading exercise
.noise
hello_friend,
This series is all about programming, code-slinging, and hacking. I'm currently preparing for a new job starting next year, so I thought it would be a good opportunity to freshen up some skillz. I've got an old drill I often used during my time at the university when learning new languages, so I thought it'd be great to enhance it a bit and share it with you. As always, you can find the code on my Github.
Before we start, you gotta have some dissipation when coding or your head might explode before you can tell the difference between the main and start functions, so here's some music from my focus folder:
Initial thoughts
As always, it's good to have some kind of plan before starting any project, or we will just fuzz around aimlessly and waste our time. So what are we going to code today? It's always a good idea to combine basic programming techniques with some more advanced stuff, so the things we are covering will be:
Multithreading
Resource access (mutex/semaphore)
Object orientation
Rng
Installation (aka a clean make file)
We are going to invoke some threads, let's call them parrots. Parrots live together in a big cage, and they have basic needs like eating, playing and taking a bath. I wrote down some requirements for the program below.
Requirements for the Parrot Program
So in short, we have different parrots, and they can use different objects based on their stats. Basically an advanced Tamagotchi. But in this relatively easy program, we can find some important mechanics used in almost every language, namely threading and spinlocks. I think you can't use a language until you mastered these important tools. In almost every modern program you'd find some form of threading, and it makes sense right? Imagine your browser dying because one Tab not responding, or a portscanner that scans one port after another. Who'd even use a program like this?
You might want to look into setting up your development environment on Linux. VSCode was really confused at first, but after I updated the path everything went smoothly.
Basic program structure (POC)
Let's first create a working prototype. Maybe this is where you draw the line, everything more is just grunt work after all ( that's not true, but I can understand the thought ).
Let's first create a parrot class representing our threads:
#ifndef Parrot_H
#define Parrot_H
#include <string>
#include "Toybox.h"
namespace ParrotDomain{
enum ParrotColor {RED, BLUE, GREEN, YELLOW};
class Parrot{
public:
Parrot(ParrotColor color, int thradId, Toybox *box);
std::string getColor() const;
void run(void);
private:
ParrotColor color_;
int threadId_;
Toybox *toybox_;
void mumble(void);
};
}
#endif
If we remember our requirements, a parrot can have one of multiple ( 4 in this case ) colors. To make sure nobody can ever fuck this up, we can create them as an enum. The parrot can return its color as a string as part of its public functions. It also has a void run which represents its lifecycle, meaning the object is more or less self-responsible for using the toybox, eating etc.
As you can see, we use include guards at the top of the header file, this is a good practice to do in every header and will prevent fatal errors stemming from somebody accidentally including a header twice, or in a loop. Also, I chose the namespace Parrotdomain, not Parrotdomain::Parrot, since I find it easier to pass around objects of the same domain like ParrotColor or the pointer to Toybox, but this is more a topic for software engineering courses, so I won't dig deeper into this topic right now. My naming conventions also are not fully dacor with Robert C. Martin's Clean Code, but I stuck to the convention of naming variables private to the class with a trailing underscore.
To make sure every thread will use the same instance of the shared resource Toybox, we pass a pointer to Toybox to each parrot in its constructor.
Now let's look at the implementation of Parrot:
#include "../include/Parrot.h"
#include <chrono>
#include <thread>
#include <chrono>
#include <random>
using namespace std;
namespace ParrotDomain{
// Mersenne Twister Engine
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<double> dis(-0.5, 1.5);
Parrot::Parrot(ParrotColor color, int threadId, Toybox *box) : color_(color), threadId_(threadId), toybox_(box) {
}
void Parrot::mumble(){
fprintf(stdout, "[ ? ] The %s parrot is mumbling (Thread %i)\n", getColor().c_str(), threadId_);
}
string Parrot::getColor() const {
switch (color_) {
case RED:
return "Red";
case GREEN:
return "Green";
case BLUE:
return "Blue";
case YELLOW:
return "Yellow";
default:
return "Unknown";
}
}
void Parrot::run(){
while (true) {
if(toybox_->try_acquire()){
fprintf(stdout, "\033[1;31m[ + ] Parrot %i is playing with the toybox ..\033[0m\n", threadId_);
std::this_thread::sleep_for(std::chrono::duration<double>(5.0));
fprintf(stdout, "\033[1;31m[ - ] Parrot %i finished playing ..\033[0m\n", threadId_);
toybox_->release();
}
//mumble chance 75 %
if(0.75 - dis(gen) >= 0) mumble();
// Generate a random delay between -0.5 and 1.5 seconds
double delay = 1.0 + dis(gen);
std::this_thread::sleep_for(std::chrono::duration<double>(delay));
}
}
}
The parrot creates a so-called Mersenne Twister Engine to create pseudo-random numbers. We could in theory create these numbers ourselves by dividing the absolute program run-time through the desired number range, but let's not make our life unnecessarily complicated at this point. Also, we will be using Dungeons and Dragons style rolls for percentage rolls in the final program, but for now just stick with the ugly if(0.75 - dis(gen) >= 0) mumble();
Let's look at the Toybox next - since we specified that 2 parrots at the same time can play with the toybox, we need to implement a Semaphore. A semaphore is a form of "traffic light" for ships. There are some differences to a classical mutex, mainly that a semaphore has a counter variable to let multiple consumers access a shared resource. Imagine programming a port scanner in a multithreading fashion. If you don't limit how many threads can access the networking resource at the same time, you will sort of DOS yourself, depending on the range of ports you want to scan.
#ifndef Toybox_H
#define Toybox_H
#include <mutex>
#include <condition_variable>
namespace ParrotDomain{
class Toybox{
public:
void release(void);
void acquire(void);
bool try_acquire(void);
private:
std::mutex mutex_;
std::condition_variable condition_;
uint8_t count_ = 2;
uint8_t max_slots_ = 2;
};
}
#endif
The variables count_ and max_slots_ will help us limit how many parrots can play at the same time, with max_slots_ acting as an upper boundary of course. Other than that we use a classical mutex and condition_variable, as you can see in the include section. Let's look at the implementation:
#include "../include/Toybox.h"
namespace ParrotDomain{
void Toybox::release() {
std::lock_guard<decltype(mutex_)> lock(mutex_);
if(count_ < max_slots_)
++count_;
condition_.notify_one();
}
void Toybox::acquire() {
std::unique_lock<decltype(mutex_)> lock(mutex_);
while(!count_) // Handle spurious wake-ups.
condition_.wait(lock);
--count_;
}
bool Toybox::try_acquire() {
std::lock_guard<decltype(mutex_)> lock(mutex_);
if(count_) {
--count_;
return true;
}
return false;
}
}
As you can see, the class itself is pretty simple, the only hard part is the sorta cryptic-looking std::lock call for our mutex. Note that Toybox::acquire uses a unique_lock while the other two functions use a lock_guard. The difference here is that with unique_lock we NEED to use the resource no matter what, so the calling thread will wait and sleep until a thread currently using the mutex is calling release and thus condition_.notify_one( ) is called which will signal the sleeping thread that it is now good to go. This behavior is a form of queue to the resource. For this program, we will mainly use try_acquire since we are simulating parrots, and a parrot who's finding the toybox used by another parrot will just move on and do something else.
Looking at the Toybox class, you have 90% of the knowledge for every other resource we offer to our parrot threads. Let's put it all together now:
#include <stdio.h>
#include <thread>
#include <vector>
#include <chrono>
#include <random>
#include "include/Parrot.h"
#include "include/Toybox.h"
using namespace ParrotDomain;
using namespace std;
// Parrot factory
Parrot createParrot(ParrotColor color, int parrotCounter, Toybox *box) {
return Parrot(color, parrotCounter, box);
}
int main()
{
vector<thread> parrotThreads;
// Create a random number generator for colorc++
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> colorDistribution(0, 3);
Toybox box;
ParrotColor parrotColor;
for (int i = 0; i < 10; i++) {
switch (colorDistribution(gen)) {
case 0:
parrotColor = RED;
break;
case 1:
parrotColor = BLUE;
break;
case 2:
parrotColor = GREEN;
break;
case 3:
parrotColor = YELLOW;
break;
}
parrotThreads.push_back(thread (&Parrot::run, createParrot(parrotColor, i, &box)));
}
for (thread &t : parrotThreads) {
t.join();
}
return 0;
}
At first, we create a factory pattern that lets us spawn parrots. It's always a good idea to create objects in this matter, some even argue that you should set your class constructor to private to deny anybody from ever spawning objects from it without the factory. If you are interested, look at this StackOverflow answer I wrote some years ago.
The main function of this program is relatively simplistic. We initialize a vector to hold our parrot threads, then roll some random numbers to determine the color of the parrot, and then create and synchronize the threads.
Note that in the push_back we specify the Parrot::run function to be our main working loop inside the class, and also we pass a reference to the Toybox to each parrot. If you are coming from Java or PHP f.e., you might not be aware of calling conventions and what's actually happening under the hood when passing around objects. In Java, for example, we implicitly create a pointer when passing an object to another object via function. But we can't be sure that is always the case in every language, and if we are unlucky, a separate object is handed to each thread via the calling stack. For a shared resource, it would be good if every thread had a reference to the same resource.
Last but not least, let's create a make file so we don't need to manually compile each class every time we want to build our program. Let's separate the object files from the binary so we can debug/reverse engineer the files if we need to:
CXX = g++
CXXFLAGS = -std=c++11 -Wall
# Source files
SRC_FILES = src/Parrot.cpp src/Toybox.cpp main.cpp
# Object files
OBJ_FILES = obj/Parrot.o obj/Toybox.o obj/main.o
# Target executable
TARGET = bin/parrotparty
all: $(TARGET)
$(TARGET): $(OBJ_FILES)
$(CXX) $(CXXFLAGS) -o $@ $^
obj/Parrot.o: src/Parrot.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
obj/Toybox.o: src/Toybox.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
obj/main.o: main.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJ_FILES) $(TARGET)
.PHONY: all clean
We define 2 targets for this make file, one called all and one called clean. The .PHONY target lets us avoid conflicts, for example, if one of our files would have the name clean.
We could work with wildcards here, but I prefer to write it this way so we can see every file, and remove some from the build process if desired. It's a matter of explicit vs. implicit.
We can now build the whole program with make -f make. Go ahead and try it. Congratulations, we now have a working proof-of-concept. In the next chapter, we will finish the program as specified, and then we will think about further improving it.
Final implementation
At this point, it would be good to get another cup of coffee and re-read the specifications. We still have some work to do and also need to review some parts of our implementation to improve readability and flow. Note that I won't show all the code here, for example, the Birdbath implementation is so similar to our Toybox that you can copy-paste it and change the name.
First, as per specification, we need to implement a Birdbath and a Foodbowl. Let's start with the Birdbath:
#ifndef BirdBath_H
#define BirdBath_H
#include <mutex>
#include <condition_variable>
namespace ParrotDomain{
class BirdBath{
public:
void release(void);
void acquire(void);
bool try_acquire(void);
private:
std::mutex mutex_;
std::condition_variable condition_;
uint8_t count_ = 1;
uint8_t max_slots_ = 1;
};
}
#endif
Since the BirdBath class will look extremely similar to our Toybox, I won't go into the specifics of the implementation. Note we only have 1 slot available and need not think about stuff like the actual color of birds that can access the resource. The Toybox, however, needs to be updated a bit:
[ ... ]
class Toybox{
public:
Toybox(std::string colour);
std::string getColour(void);
void release(void);
void acquire(void);
bool try_acquire(void);
private:
std::string colour_;
std::mutex mutex_;
std::condition_variable condition_;
uint8_t count_ = 2;
uint8_t max_slots_ = 2;
[ ... ]
#include "../include/Toybox.h"
namespace ParrotDomain{
Toybox::Toybox(std::string colour): colour_(colour){}
std::string Toybox::getColour(){
return colour_;
}
[ ... ]
Since the Toybox needs to take into account the color of the birds that can play with it, we need to update the class a bit to represent this behavior.
Our next shared resource is the Foodbowl, allowing one bird at a time to eat there. It also has functionality to refill it and a capacity, although I did not implement a way of refilling it here. This would be something you can write on your own, as an easy exercise.
#ifndef Foodbowl_H
#define Foodbowl_H
#include <mutex>
#include <condition_variable>
namespace ParrotDomain{
class Foodbowl{
public:
Foodbowl(int maxVal);
void fill(int value);
void release(void);
void acquire(void);
int eat_from(int amount);
bool try_acquire(void);
bool is_empty(void);
private:
int maxVal_;
int currVal_;
std::mutex mutex_;
std::condition_variable condition_;
uint8_t count_ = 1;
uint8_t max_slots_ = 1;
};
}
#endif
Again, the semaphore is very similar to our Toybox except having a maximum capacity and the option to refill it. Let's take a look at it's implementation:
#include "../include/Foodbowl.h"
namespace ParrotDomain{
Foodbowl::Foodbowl(int maxVal): maxVal_(maxVal){}
void Foodbowl::fill(int amount){
if(amount <= 0 || amount >= INT32_MAX)
return;
acquire();
if((currVal_ + amount) > maxVal_)
currVal_ = maxVal_;
else
currVal_+=amount;
release();
}
int Foodbowl::eat_from(int amount){
int retVal = amount;
if(currVal_ - amount <= 0)
{
retVal = amount - currVal_;
currVal_ = 0;
}
else
currVal_-=amount;
return retVal;
}
bool Foodbowl::is_empty(){
return currVal_ == 0 ? true : false;
}
void Foodbowl::release() {
std::lock_guard<decltype(mutex_)> lock(mutex_);
if(count_ < max_slots_)
++count_;
condition_.notify_one();
}
void Foodbowl::acquire() {
std::unique_lock<decltype(mutex_)> lock(mutex_);
while(!count_) // Handle spurious wake-ups.
condition_.wait(lock);
--count_;
}
bool Foodbowl::try_acquire() {
std::lock_guard<decltype(mutex_)> lock(mutex_);
if(count_) {
--count_;
return true;
}
return false;
}
}
When refilling, the FoodBowl will lock itself so no parrot can eat while we fill in new food. Again, don't think about this right now, this is your homework ;)
We also have a function to check whether the bowl is empty and to let a parrot eat from it. When eating from the bowl, the bowl must check whether it has enough food left so the bird can fully eat up, else we have to calculate the difference. The bowl will return to the parrot thread an amount of food it can consume and update itself accordingly.
With this, we have covered all our shared resourced for the program, let's take a look at the final parrot object:
#ifndef Parrot_H
#define Parrot_H
#include <string>
#include "Toybox.h"
#include "BirdBath.h"
#include "Foodbowl.h"
namespace ParrotDomain{
enum ParrotColor {RED, BLUE, GREEN, YELLOW};
class Parrot{
public:
Parrot(
ParrotColor color,
int thradId,
Toybox *box = nullptr,
BirdBath *bath = nullptr,
Foodbowl *foodbowl = nullptr
);
std::string getColor() const;
void run(void);
private:
ParrotColor color_;
int threadId_;
int boredom_;
int stomache_food_;
int poopcounter_;
int stamina_;
int sleeptimer_;
int skipcycle_ = 0;
Toybox *toybox_ = nullptr;
BirdBath *bath_ = nullptr;
Foodbowl *foodbowl_ = nullptr;
void mumble(void);
void play(void);
void bath(void);
void poop(void);
void eat(void);
void checkStamina(void);
};
}
#endif
I choose to go for only one constructor for the parrot, with the option to pass it a nullptr reference. This is useful if we want to test stuff, or if parrots don't get to have a BirdBath for example. In theory, we do not need to pass it any shared resources, but since we update our parrot to become hungry in this step, it would not be wise to deny them the option to eat. You can also see that the parrot has some new stats, like boredom and stamina. These values will be used to increase the parrot's need to access some resources, f.e. a bored bird will be more likely to play with the Toybox than one that just finished playing with it.
Let's look at the parrot implementation, probably the most complex class in this whole program so far:
#include "../include/Parrot.h"
#include <chrono>
#include <thread>
#include <chrono>
#include <random>
using namespace std;
#define COLOR_RED "\033[1;41m"
#define COLOR_GREEN "\033[1;42m"
#define COLOR_BLUE "\033[1;44m"
#define COLOR_YELLOW "\033[1;43m"
#define COLOR_YELLOW_FOREGROUND "\033[1;93m"
#define COLOR_BLUE_FOREGROUND "\033[1;94m"
namespace ParrotDomain{
/////////////////// Mersenne Twister Engine (random generator) ////////////////////////////////
std::random_device rd;
std::mt19937 gen(rd());
//some number distributors to play around with
std::uniform_real_distribution<double> sleep_distributor(-0.5, 1.5); //sleep timer
std::uniform_real_distribution<double> playTime_distributor(2.0, 10.0);
std::uniform_real_distribution<double> mumblechance(1, 20); // dnd style chance calculation (D20 ; 20 = 5%)
std::uniform_real_distribution<double> playfullness(1, 20);
std::uniform_real_distribution<double> *bathneed = &playfullness; //distributor can be pointer
std::uniform_real_distribution<double> *bathtime_distributor = &playTime_distributor;
std::uniform_real_distribution<double> *poop_distributor = &playTime_distributor;
std::uniform_real_distribution<double> food_distributor(0.0, 5.0);
std::uniform_real_distribution<double> stamina_distributor(15.0, 25.0);
/////////////////////////////////////////////////////////////////////////////////////////////////////////
//multi purpose constructor that will accept nullptr
Parrot::Parrot(
ParrotColor color,
int threadId,
Toybox *box,
BirdBath *bath,
Foodbowl *foodbowl
):
color_(color),
threadId_(threadId),
toybox_(box),
bath_(bath),
foodbowl_(foodbowl)
{
stomache_food_ = 10 + (int)food_distributor(gen);
poopcounter_ = (*poop_distributor)(gen); //calling rng generator via pointer
stamina_ = (int)stamina_distributor(gen);
sleeptimer_ = stamina_;
}
string Parrot::getColor() const {
switch (color_) {
case RED:
return COLOR_RED;
case GREEN:
return COLOR_GREEN;
case BLUE:
return COLOR_BLUE;
case YELLOW:
return COLOR_YELLOW;
default:
return "Unknown";
}
}
////////////////////////////// Parrot main working function /////////////////////////////
void Parrot::run(){
//parrot lifecycle. certain activities will be more likely to occure based on parrot's internal state
while (true) {
//parrot takes a moment to think or dream..
double delay = 1.0 + sleep_distributor(gen);
std::this_thread::sleep_for(std::chrono::duration<double>(delay));
checkStamina();
if(!poopcounter_--)
poop();
if(skipcycle_){ //can parrot act? (test against 0)
if(stamina_==0)
continue;
//mumble chance 10 % as per DND dice roll
if(mumblechance(gen) >= 19) mumble();
if(!stomache_food_) {
eat();
continue;
}
//boredom will increment over time (chance increments from 5% upwards + 2.5% / cycle)
if(playfullness(gen) + (int)(boredom_++ / 2) >= 19) play();
//random distributor used pointer style ( chance 10% )
if((*bathneed)(gen) >= 19) bath();
}
else
skipcycle_--;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
void Parrot::mumble(){
fprintf(stdout, "%s[ ? ] The parrot is mumbling (Thread %i)\033[0m\n", getColor().c_str(), threadId_);
}
void Parrot::play(){
if(toybox_ == nullptr)
return;
if(toybox_->try_acquire()){
fprintf(stdout, "%s[ + ] Parrot %i is playing with the toybox ..\033[0m\n", toybox_->getColour().c_str(), threadId_);
std::this_thread::sleep_for(std::chrono::duration<double>(3.0 + playTime_distributor(gen)));
boredom_ = 0;
toybox_->release();
}
stamina_-=5;
}
void Parrot::bath(){
if(bath_ == nullptr)
return;
if(bath_->try_acquire()){
fprintf(stdout, "%s[ + ] Parrot %i is taking a bath ..\033[0m\n", COLOR_YELLOW_FOREGROUND, threadId_);
std::this_thread::sleep_for(std::chrono::duration<double>(5.0 + (*bathtime_distributor)(gen)));
bath_->release();
}
stamina_-=3;
//parrot needs some time to dry
skipcycle_ = 6;
}
void Parrot::poop(){
if(!stomache_food_)
return;
poopcounter_ = (int)(*poop_distributor)(gen);
stomache_food_--;
}
void Parrot::eat(){
if(foodbowl_ == nullptr || foodbowl_->is_empty())
return;
if(foodbowl_->try_acquire()){
//give the parrot some randomness in how statiated it is
int food_amount = foodbowl_->eat_from(10 + (int)food_distributor(gen));
stomache_food_ = food_amount;
//simulate the eating process ( 1 second per food unit )
for ( int c = 0; c < food_amount; c++){
std::this_thread::sleep_for(std::chrono::duration<double>(1.0));
}
fprintf(stdout, "%s[ + ] Parrot %i has eaten %i units of food ..\033[0m\n", COLOR_YELLOW_FOREGROUND, threadId_, food_amount);
foodbowl_->release();
}
stamina_-= 3;
//parrot needs some time to rest after eating
skipcycle_ = 4;
}
void Parrot::checkStamina(){
if(stamina_>0){
stamina_--;
}
else{
sleeptimer_--;
if(sleeptimer_ == 0){
fprintf(stdout, "%s[ + ] Parrot %i is waking from slumber ..\033[0m\n", getColor().c_str(), threadId_);
stamina_ = (int)stamina_distributor(gen);
sleeptimer_ = stamina_;
skipcycle_ = 2; //chill for a moment after waking up
}
}
}
}
That's 200 lines of code, I know. But don't fret, much of it is not that hard to get. The color codes at the start are just to color console output and will increase output readability, so you can ignore it. Let's start with the big block of random generators at the top.
I tried to show different approaches to random generation here. The first two distributors are much like the one we already know from the prototype. The next two, mumblechance and playfullness, are D20 dice rolls analog to Dungeons and Dragons (therefore starting at 1, not 0). I think it's easier to imagine rolling a dice than having cryptic numbers as a range. What this means is that a 20 has a 5% chance of getting rolled. This is much easier to imagine. So a check for 10% would be if(mumblechance(gen) >= 19) mumble();
Feel free to play around with this, you can even give the parrot a +1 to its roll, in fact, we are using a similar mechanic to simulate the parrot getting more and more bored over time.
The next 3 distributors are pointers to already existing distributors. Why use a reference to an object we already have, you might ask. For once, because we can! This program is an exercise, after all, so checking out all available options is a good way of getting better at the language. Also, it is really helpful to have the distributor for pooping have its own name instead of calling the same one again and again.
The constructor is relatively easy to get, given its strange formatting. I really hate too long lines of code, so I tend to stack downwards instead ^^
Let's now look at the main worker function: run.
The parrot will keep track of its internal state on its own, with the stamina being the first one to check. We don't need any shared resources for this, so it's pretty easy to follow. I decided to give the parrot a stamina equaling its sleeptime, so if the parrot is exhausted, it will sleep until its stamina is refilled, with some rng each time it wakes up. I admit this is the ugliest implementation in this class, so feel free to refactor it, I kind of did this because I was explaining stuff to a beginner at the time so I tried to keep it simplistic.
Pooping will work without a shared resource too, slowly emptying its stomach. I decided to randomize the time it takes for the next pooping to occur, this will make the parrot behave more naturally.
Eating and taking a bath both have an internal timer, so the thread is actually sleeping to simulate the chosen action. If the parrot decides it would be a good time to take a bath, for example, it will try acquiring the shared resource. As already mentioned, we don't NEED to access the resource, so if another bird is currently blocking it, we just move on. For this reason, we must always release the resource manually after locking it.
To not further increase chaos, I decided it would be enough to just increment boredom from time to time, feel free to do the same for bathneed and mumblechance if you so desire. After each major activity, the bird takes some cycles to space out, represented by the skipcycle counter. I wanted to really try and provoke the threads to lock each other out of the shared resources.
Lastly, let's take a look at the final main function:
#include <stdio.h>
#include <thread>
#include <vector>
#include <chrono>
#include <random>
#include "include/Parrot.h"
#include "include/Toybox.h"
#include "include/BirdBath.h"
#include "include/Foodbowl.h"
using namespace ParrotDomain;
using namespace std;
// Parrot factory
Parrot createParrot(ParrotColor color,
int parrotCounter,
Toybox *box = nullptr,
BirdBath *bath = nullptr,
Foodbowl *foodbowl = nullptr)
{
return Parrot(color, parrotCounter, box, bath, foodbowl);
}
int main(){
vector<thread> parrotThreads;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<int> colorDistribution(0, 3);
//parrot interaction objects
Toybox box_1("\033[1;31m");
Toybox box_2("\033[1;34m");
BirdBath bath;
Foodbowl foodbowl(500);
//parrot creation
ParrotColor parrotColor;
for (int i = 0; i < 10; i++) {
Toybox *boxPtr;
switch (colorDistribution(gen)) {
case 0:
parrotColor = RED;
boxPtr = &box_1;
break;
case 1:
parrotColor = BLUE;
boxPtr = &box_1;
break;
case 2:
parrotColor = GREEN;
boxPtr = &box_2;
break;
case 3:
parrotColor = YELLOW;
boxPtr = &box_2;
break;
}
parrotThreads.push_back(thread (&Parrot::run, createParrot(parrotColor, i, boxPtr, &bath, &foodbowl)));
}
//parrot synchronization
for (thread &t : parrotThreads) {
t.join();
}
return 0;
}
Not that much new stuff here, we are making sure that a red or blue bird can only play with the first toybox, and a green or yellow bird can only play with the second. Believe it or not, I had test runs where 12 birds were created with only red and blue colors. The drill is really big on random number generation somehow.
Update the make file to include our new files
CXX = g++
CXXFLAGS = -std=c++11 -Wall
# Source files
SRC_FILES = src/Parrot.cpp src/Foodbowl.cpp src/Toybox.cpp src/BirdBath.cpp main.cpp
# Object files
OBJ_FILES = obj/Parrot.o obj/Foodbowl.o obj/Toybox.o obj/BirdBath.o obj/main.o
# Target executable
TARGET = bin/parrotparty
all: $(TARGET)
$(TARGET): $(OBJ_FILES)
$(CXX) $(CXXFLAGS) -o $@ $^
obj/Parrot.o: src/Parrot.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
obj/Toybox.o: src/Toybox.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
obj/BirdBath.o: src/BirdBath.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
obj/Foodbowl.o: src/Foodbowl.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
obj/main.o: main.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJ_FILES) $(TARGET)
.PHONY: all clean
And give it a try:
As you can see, the parrots are behaving more or less independently. I'm not quite sure why the quality on YouTube is so miserable, but you can see the most important parts I assume.
Where to go from here
If you really want to go over the top with this, here are some ideas on how you can further enhance the program:
give the user some form of input option. You could for starters have a console command to refill the food bowl
give the user the option to cuddle or play with a specific bird
give the user the option to talk to the birds, and have the birds repeat phrases the user input during a mumble call
track an internal happiness of each bird, that goes down if the parrot can't eat or play over a certain amount of time. the bird could screech if it is unhappy and mumble more often if it is happy
give 2 birds an option to become friends / partners. If a bird plays with its partner, it will become very happy. You could go further and give the parrots a gender so they can even become partners and hatch out a little baby bird.
create a GUI for the program that shows parrots in their different activities. You can make a rich game with this or create a simple Tamagotchi-style view for each bird
I did not go all the way to a full-fledged parrot simulation here since this is only a drill for me, but if anybody did this I would really appreciate seeing it, so feel free to leave a comment or write me.
Final thoughts
I really like doing these sorts of drills from time to time, to refresh my knowledge of the different languages I'm using. I think that this exercise covers many important features of any language, so if you code this yourself or have similar drills, I'd be happy if you shared yours.
Thanks for reading this article, feel free to comment your thoughts below. Also, if you think you can improve the code or have big issues with my chaotic coding style, feel free to fork the repository or just write me a message.
Subscribe to my newsletter
Read articles from clockwork directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by