codinglikejesus
codinglikejesus

Reputation: 60

How to allow multiple threads to access critical section that match condition in C++

![railroad]: http://www.cs.tut.fi/~rinn/htyo/design/pics/railroad.png

I'm trying to create a simulation software of trains riding the railway shown above. The concurrency issues stem from the following boundry conditions:

*Each track section is ONE WAY at a time: when a train has started going into one direction, there can not be any other trains going in opposite direction (note: this allows multiple trains in the SAME direction).

*When train has started on a section it SHOULD run to next section/junction (in other words: no U-turn in the middle of track section)

*Trains do NOT have to have a specific schedule (their overall movements can be random in the whole track-network).

*The track exchange/switching locations can be "magic" in the sense that any number of trains can be on them (waiting), all trains can pass each other to any direction inside them.

My plan would be to create 2 variables, one which would contain the current direction of trains on the track in hand and a second one which would count the amount of trains on the track. The idea would be to first check if the train is allowed to enter the track (in other words, direction on track == direction of departing train OR if track is empty) and then increment the count of trains on the track. When the last train on the track arrives to the next junction, the direction-variable would be set to 0 allowing any trains to access the track.

As far as I can tell, I'd have to create my own locking mechanism of some sort to be able to implement this, but I'm lost on the specifics of the implementation. Condition variables seem like they could be useful, but all the advice i've read use a mutex with it, which would not be suitable for our case. Maybe a sempahore with a condition variable?

In case these are helpful, here's the implementation so far: railroad.cc

#include <chrono>
#include <thread>
#include <iostream>
#include <mutex>
#include <atomic>
#include "railroad.hh"


std::atomic_int lock = 0;
std::mutex cout_mutex;
Railroad::Railroad(std::vector<Track> tracks, std::vector<int> junctions, std::vector<Train> trains):
    tracks(tracks), junctions(junctions), trains(trains)
{

}

bool Railroad::moveTrain(int id)
{
    for(int i = 0; i < 10; i++){
        std::this_thread::sleep_for(std::chrono::milliseconds(std::rand()%300));
        bool finished = false;
        int target_id = rand()%6;
        int start;
        for(auto train_it = trains.begin(); train_it != trains.end(); train_it++){
            if(finished){
                break;
            }
            if(train_it->getId() == id){
                start = train_it->getLocation();
                train_it->setMoving(true);
                for(auto track_it = tracks.begin(); track_it != tracks.end(); track_it++){
                    if(track_it->id == target_id){
                        finished = true;
                        if(start == track_it->point2){
                            track_it->in_use == true;
                            cout_mutex.lock();
                            std::cout << "Train " << id
                                      << " started moving on track "
                                      << target_id << std::endl;
                            cout_mutex.unlock();
                            std::this_thread::sleep_for(std::chrono::milliseconds(std::rand()%1000));
                            train_it->setLocation(track_it->point1);
                            train_it->setMoving(false);
                            cout_mutex.lock();
                            std::cout<< "Train " << id << " has arrived from "
                                     << start << " to " << track_it->point1
                                     <<std::endl;
                            cout_mutex.unlock();
                            break;
                        }
                        else if(start == track_it->point1){
                            track_it->in_use == true;
                            cout_mutex.lock();
                            std::cout << "Train " << id << " started moving on track "
                                      << target_id << std::endl;
                            cout_mutex.unlock();
                            std::this_thread::sleep_for(std::chrono::milliseconds(std::rand()%1000));
                            train_it->setLocation(track_it->point2);
                            train_it->setMoving(false);
                            cout_mutex.lock();
                            std::cout<< "Train " << id << " has arrived from " << track_it->point1 << " to " << track_it->point2 <<std::endl;
                            cout_mutex.unlock();
                            break;
                        }
                        else{
                            cout_mutex.lock();
                            std::cout<< "Train " << id << " cannot access "<<track_it->id << std::endl;
                            cout_mutex.unlock();
                            break;
                        }
                    }
                }
            }
        }
    }
}


main.cpp

#include <iostream>
#include <memory>
#include <random>
#include <thread>
#include "railroad.hh"


using namespace std;

int main()
{
    std::vector<Track> tracks;
    tracks.push_back({0,0,2, false});
    tracks.push_back({1, 2, 3, false});
    tracks.push_back({2, 3, 0, false});
    tracks.push_back({3, 0, 1, false});
    tracks.push_back({4, 1, 2, false});
    tracks.push_back({5, 1, 3, false});

    std::vector<int> junctions;
    std::vector<Train> trains;

    trains.push_back({0,0,false});
    trains.push_back({1,1,false});
    trains.push_back({2,2,false});
    junctions.push_back(0);
    junctions.push_back(1);
    junctions.push_back(2);
    junctions.push_back(3);
    Railroad* railroad = new Railroad(tracks, junctions, trains);

    std::thread t1(&Railroad::moveTrain,railroad,0);
    std::thread t2(&Railroad::moveTrain,railroad,1);
    std::thread t3(&Railroad::moveTrain,railroad,2);


    t1.join();
    t2.join();
    t3.join();
}

Upvotes: 0

Views: 564

Answers (1)

Michael Kenzel
Michael Kenzel

Reputation: 15951

Basically, what you want is for a train that seeks to enter a specific track to wait until there is either no train on track or the direction of all trains on track is the same as the direction your train wants to run in. One way to solve this would be using an std::condition_variable. A condition variable allows you to block a thread until a particular condition becomes true (hence the name). For each track, you have a counter counting the number of trains on the track. You can use either a separate variable or just the sign of the counter to keep track of the current direction on the track (e.g., counter clockwise counts positive, clockwise negative). Whenever a train wants to enter a track, it checks if the counter is zero or the direction is the same as the direction it wants to go in. If this is not the case, it waits for this condition to become true. Since the counter may be modified concurrently, access to the counter has to be locked with a mutex. You acquire the mutex and check the counter. If the counter is such that you may enter the track, you update the counter, unlock the mutex, and proceed. If the counter is such that you cannot currently enter the track, you call wait on your condition variable. When calling wait, you have to hand over the lock you're currently holding. The wait operation on the condition variable will atomically put your thread to sleep and release the mutex so that other threads may do stuff with the counter in the meanwhile (otherwise, the condition could never become true). Whenever a train exits the track, it will acquire the lock, update the counter, and, if it was the last train to exit, notify the condition variable. This notify operation will wake all threads currently waiting on the condition variable. In each of these threads, the wait call the thread blocked on will reacquire the mutex and return. Thus, the threads, one by one, get to check again if the condition they were waiting on happens to be true now. If yes, they may proceed, if not, they continue to wait.

It would probably be best to encapsulate all of this in a class, for example:

#include <mutex>
#include <condition_variable>

enum class Direction
{
    CCW = 1,
    CW = -1
};

class Track
{
    int counter = 0;

    std::mutex m;
    std::condition_variable track_free;

    static int sign(Direction direction)
    {
        return static_cast<int>(direction);
    }

    bool isFree(Direction direction) const
    {
        return (sign(direction) > 0 && counter >= 0) || (sign(direction) < 0 && counter <= 0);
    }

public:
    void enter(Direction direction)
    {
        std::unique_lock lock(m);

        while (!isFree(direction))
            track_free.wait(lock);

        counter += sign(direction);
    }

private:
    bool release(Direction direction)
    {
        std::lock_guard lock(m);
        counter -= sign(direction);
        return counter == 0;
    }

public:
    void exit(Direction direction)
    {
        if (release(direction))
            track_free.notify_all();
    }
};

and then your trains just call, e.g.,

track.enter(Direction::CW);

to enter a track and

track.exit(Direction::CW);

when they're leaving…

Upvotes: 1

Related Questions