Introduction
In my last semester, our OS design teacher gave us an assignment to do at the end that revolved around synchronizing threads and so on.
Assignment description
We were tasked with simulating a train station that had 3 stations, 3 tracks, 6 trains, and 2 trains per track.
A train’s sole behavior is going forward in the track, NO LOOKING BACK.
The mutual exclusion part comes from the fact that, of course, no two trains can occupy the same Cell
. (a track is made out of a bunch of Cell
objects …)
Oh, also, we needed to implement it in Java, so … Yeah …
The assignment solution
We (what me and my partner ended up doing (although to be honest he did most of the programming because at first i kind of did not understand semaphores and ended up procrastinating)) ended up with a solution that roughly did something like this:
- implemented
Train
,Cell
,Station
, andTrack
classes. - Cell represents a portion of the track that can be occupied by a train, its most important fields, a semaphore protecting the cell, other than that the crucial function in regards to this application (if you can call it that) are: enter and leave, to model the actions a train can take.
- A Station is a special Cell.
- A track is a bunch of Cells.
- Train had some private field like the train name, but the most important thing in this class is the thread its going to run, and, since this is Java we’re talking about, this class naturally inherits Thread (I mean personally i would’ve made it implement the Runnable interface, but hey, that’s just me, and i have to obey the instructions given to me …) and the run() method is arguably the most central/crucial part of the application.
run()
So, the functionality implemented in this method is actually kind of simple, but it’s crucial to understand nonetheless :
1) The train calculates the next position.
2) It selects the corresponding cell from the array of the Track.
3) It locks the semaphore of that Cell
4) It enters the cell and does its business.
5) It leaves the cell
6) It releases the semaphore.
That’s … Yeah that’s pretty much it.
Thoughts on the Java solution
So, i thought on providing the actual code, but i think it’s way too much effort to paste it here and make sure its well formatted and then translating it.
So i won’t bother, i will just be making this comment :
I felt the entire thing was a hard-coded hack and i was not pleased with it, the constant number of trains, the way the constructors was made … Just … I don’t even think objects are necessary to be honest ..
Plus, i wanted to have a better understanding of the way things worked …
And so, i decided to try implementing the assignment in a language that i haven’t touched in quite a while (certainly more than three years).
I have decided ti implement it .. … (suspense builds before you remember the title of this post)
… C++
So, let’s get right into it:
- One source file and one header file per class.
- One main.cpp file.
That was the initial plan, anyways …
The classes
Train, Track, and Cell.
Just those three classes, my idea was that i can control for whether or not a cell is a station or not using a boolean … bad idea
Other than that, the train and track classes overall have the same functionality.
The header files
I don’t think i will be citing all the source code, … but i think i should at least include the headers.
Train
class Train {
public:
std::string m_trainName;
int m_initialPos;
int m_currentPos;
Track * m_associatedTrack; // Declaring this as a pointer makes me feel like i might make this harder for me than is necessary
std::thread m_circulate;
// -------------------------------------------------- //
Train();
~Train();
Train(int, Track *, std::string);
void circulate();
};
Track
class Track {
public:
int m_trackLength;
std::vector<Cell> m_actualTrack; // 2019-12-22 15:05 zenAndroid -- Just decided to turn this into a Cell-pointers vector
// I can see the bullet hole in my foot already
Track();
Track(int, Cell, Cell, Cell, int, int, int);
// This Constructor is a fucking disgrace, however i'll run with this at first then i assume i'll set up a map or something ...
// Although I don't even fucking know if that'd be a decent way to solve it.
// Still, better than fucking hardcoding it in the constructor, I bet.
};
Cell
enum class CellType { CELL = 1 , STATION };
class Cell {
public:
std::string m_cellName; // Guess
CellType m_type;
// Mutex protecting the *specific instance* of the cell.
std::mutex* m_cellKey;
// ----------------------------- //
Cell();
Cell(CellType);
void promoteCell(std::string); // Upgrading a cell to station level.
void enter(std::string); // Enter and leave this cell.
void leave(std::string);
};
Everything’s public, that’s not exactly the, uh .. ideal way
shrug I mean, given the atomic nature of this personal project (literally just me working on it), i thought that I’d do it since i can manage to recall the source code all in my head, so there should be no problem with declaring them as public fields, and i didn’t think too much about it, but … it does make me wonder .. what’s the point of private fields?
Presumably to protect the field such that you would have to create a getter/setter pair for it … but how does that stop someone from fiddling with a field if they wanted to? Can’t they just write to it using the setter, and likewise read from it using the getter ?
Maybe it’s to prevent errors while testing or something?
I’m sure I’m missing something.
UPDATE : I am indeed missing something: see below.
[3:52 AM] internetHandle : Let's say you have a person class with an age field, if you later change the layout to instead use a year-of-birth field, you have to go back and change every person.age acces to person.year-of-birth plus whatever processing to get the right value (age in this case)
[3:52 AM] internetHandle : But if you use a g/s pair, you could just redefine the "get-age" method to calculate the age instead, needing to change nearly nothing
[12:42 PM] zenAndroid: I can't fucking believe this slipped out of my mind
[12:42 PM] zenAndroid: i cant fucking believe it
[12:42 PM] zenAndroid: fugg
Back to our topic
Anyways, the Cell class has the std::mutex* m_cellKey;
field, which i did because when i tried the normal declaration, at the header making statements like std::mutex m_cellKey
the compiler kept complaining about how std::mutex was non copyable and non movable, decided to try this pointer declaration method, it stopped whining, and here we are.
Will of course ask around to figure out what the heck that was about, soon
Train class hes a pointer to the associated track, because this isn’t Java, so yeah you have to do this stuff.
Still don’t understand why people hate pointers and such, they are difficult when they get hyper-hyper indirect and such but ehh
Also, i have just realized that my application leaks quite a bit of memory since I’m not freeing my pointers, …
Will fix.
Also unlike Java, instead of the class inheriting directly from Thread/implementing Runnable, i had and std::thread
as a field of the class, which i initialized in the constructor, and .wait()
ed for in the destructor.
I have initialized the thread with the circulate()
function, I will of course go in more depth about it.
Track class is straight forward, length of the track and the actual array holding the cells.
circulate()
void Train::circulate() { // The functionality of the thread spawned by the instance of the class
while(1){
// TODO : something
// Need to calculate the next position the train is going to take.
m_currentPos = (m_currentPos + 1) % (m_associatedTrack->m_trackLength);
// This is how you access the corresponding cell : m_associatedTrack->m_actualTrack[m_currentPos];
// For now I don't want to confuse myself so I'll be doing the oprations chained together
// until i grasp how i can do stuff like Cell = [that statement]; without hecking myself up
//
// ... Let me think
m_associatedTrack->m_actualTrack[m_currentPos].enter(m_trainName);
// Presumably enters the cell
m_associatedTrack->m_actualTrack[m_currentPos].leave(m_trainName);
}
} //
Huh, as I am writing this blog post I am realizing what I’ve programmed isn’t all that impressive …
Oh well.
Wonder why this seemed like a more impressive thing in Java.
Anyways so yeah, the circulate function does what i outlined it doing before.
Although i might have to remove the leave function, given that when the train finishes the enter() function then it pretty much left anyways, and there is no need to clutter up the code and make the 1-to-1 correspondence to real life harder that it is.
Did that make sense ?
It didn’t make sense huh?
What i mean is that i aim to make the code as accurately representative of reality as possible.
Because i think that’ll actually make it easier to understand.
IE i want the code to model reality as closely as possible.
m_circulate = std::thread(&Train::circulate,this);
The way to initialize threads is not completely intuitive, but i found this answer on stack overflow.
Rubs me the wrong way when i take code from the interwebs and patch it in without understanding it, so I headed to cppreference to understand what i just did.
thread() noexcept; (1) (since C++11)
thread( thread&& other ) noexcept; (2) (since C++11)
template< class Function, class… Args > explicit thread( Function&& f, Args&&… args ); (3) (since C++11)
thread(const thread&) = delete; (4) (since C++11)
So, obviously (wasn’t actually obvious to me) number (3) is applicable here.
But yeah, TL;DR :: that constructor takes a function and an arguments, and it takes them by reference (or something .. :sob:)
Rules out initializing a thread with a function that takes an argument, so you pass in a function that takes none, and you put the arguments as class fields, with would be accessible since you’re passing this
too.
All right, i don’t see how to expand on this any further.
Planned improvements
- Add new class Station.
- Initialize the tracks with an std::map, rather that hardcoding it.
- See if you need classes and OOP at all (i suspect not).
Conclusion
In conclusion, this isn’t the end, i will come back regularly to improve on this codebase.
I am glad i did this, I walked into this not knowing much about how to do multithreading in C++, and walked out with some semblance of an idea, while i am not under any illusion that i know much about the topic, i can at least say how to create the equivalent of a synchronized block in C++, and how to initialize a thread, which i am grateful for.
As of this writing, i am on this commit in my github repository.