Reference : https://media3.giphy.com/media/cH5qB4vqM8NoI/source.gif

Concurrency and Threads: Real Life Examples — Part 2

Tarun Kundhiya
Geek Culture
Published in
9 min readApr 11, 2021

--

Previously, you saw how a multithreaded system brings complexities with it and how can we solve it using synchronization. Now let’s see how threads can talk to each other.

I would suggest those who haven’t read that Part 1 yet, please go through it to understand the next story without any suspense.

However, these are the key concepts that we learned last time.

  1. Shared Variable: a variable that is shared by multiple threads.
  2. Critical Section: a section of the code involving a shared variable that is accessed by multiple threads
  3. Race Condition: a situation where we don’t know the final value of the shared variable that occurs over a critical section.
  4. Synchronization: a solution to a race condition that lets one thread run at a time over a critical section.

One key point to understand here is the problem related to the critical section or the race condition occurs only when more than one thread is updating the shared variable. The problem is not with multiple threads reading but multiple threads writing.

So what if we restrict the update to that particular object somehow, we only return an updated copy of the shared variable without updating the original one.

This is nothing but Immutability and this is how we can simply achieve it.

public class ImmutableValue {private int value = 0;public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public ImmutableValue add(int valueToAdd){
return new ImmutableValue(this.value + valueToAdd);
}

}

Now if any thread calls the add method on this object, it would not update the internal variable ‘value’ but would get a new Immutable Object with the updated value. So every thread would have its own copy of the updated variable.

I wonder that when this would ever be updated. I guess if there is any safe manual process of updating that variable then we can use it as a shared variable without much reluctance.

I believe that the git branching strategy that we are using is a classic example of this.

The strategy is we take a feature branch from the master branch and never update the master branch directly but through an approval process.

So the pseudo code for it would look just like this.

public class MasterBranch extends Branch{private Code code ;// Initial setup of the branch
public MasterBranch(Code code){

if(this.code != null){
this.code = code;
}else{
throw new Exception("Master branch is already set up")
}
}
public Code getCode() throws Exception {
throw new Exception("Operation not supported");
}
// git checkout -b "feature/super-cool-feature"
public Branch createBranch() {
Code code = code.clone(); // Deep clone
return new Branch(code);
}
// git merge feature/super-cool-feature
public void merge(Branch featureBranch) throws CodeError {

if(featureBranch.isApproved()){
this.code.merge(featureBranch.getCode());
} else {
throw new CodeError("");
}

}

}

You will notice here the code object of the master branch is never updated unless someone approved the feature branch.

This approval process is supposed to take care of multiple branches updating the master branch one at a time.

One must have realized how hectic this approval process is, it’s like waiting for your friend to approve your request while he is reviewing some other pull requests.

Finally your turn. Then he says ‘buddy I see merge conflicts for your PR with Robin’s

https://imgflip.com/i/3tl76e

Gee, that’s it. So many complications even with a man involved. Imagine what would happen when we would automate this?

Just think for a minute, what if robin would have communicated to Batman here that he is working on the same codebase. We could definitely have saved a slap.

On the same note, what if two threads start to communicate with each other.

Communicating threads. Wow, that’s interesting.

Yeah, you heard Leslie. We can just use a shared variable.

So let’s do it. Below is another illustration to better understand this communication process.

Let’s consider a hypothetical situation of two trains Train A and Train B. They arrive at the same station at 10:00 pm and the other right after 10 minutes at 10:10 pm.

Let’s say the Train-A stops at the station for 5 minutes on average and then after it passes Train-B occupies the station. This is a short simulation code where each train acts like a thread.

public class Signal {
public boolean trainAPassed;
}
class TrainA extends Threads { private Signal signal; public TrainA(Signal signal){

this.signal = signal;
System.out.println("Train A : Run Engine");
}

@Override
public void run() {
System.out.println("Train A : Pass from the station"); // Waiting time at the station
Thread.sleep(5*60*1000);
System.out.println("Train A : Passed from the station");
Signal.trainAPassed = true;
}
}class TrainB extends Threads { private Signal signal; public TrainB(Signal signal){
this.signal = signal;
System.out.println("Train B : Run Engine");
}

@Override
public void run() {
// Wait while the train A is not passed
while(!Signal.trainAPassed){
System.out.println("Waiting for train A to be passed");
}
System.out.println("Train B : Pass from the station");
System.out.println("Train B : Passed from the station");
}
}

Now suppose the Train-A arrives and little late at the station say at 10:09 pm and but Train-B is on time. What would happen in this case?

Train-B needs to wait to enter the station till 10:14 pm right. 4 minutes of halt before the station.

If you start the threads you would definitely see this message,

"Waiting for train A to be passed"

and repeatedly for 4 minutes.

It means Train-B keeps running its engine even when it has nothing to do still burning up the electricity for nothing, in other words for a thread, consuming the CPU time.

Remember every thread running consumes some time of the CPU even it does nothing but print.

This is definitely a bad design which is called Busy waiting in technical terms. Why should Train-B have to keep its engine running while Train-A has not passed? Right!

Can we not put that Train-B/Thread-B to a state where it does not take the CPU time and still doesn’t die. Is there such a state for a thread?

Yes, there is such a state. It’s called the waiting state.

And Java and many other languages provide ways to put a thread in such a state.

This is a good article listing all the thread states that we can achieve.

This is an image from the above article displaying all the states a thread could achieve.

So the code would change to something like this

class TrainA extends Threads {....
....
....
@Override
public void run() {
System.out.println("TrainA : Pass from the station"); Thread.sleep(5*60*1000); System.out.println("TrainA : Passed from the station");

signal.notify();
}
}
class TrainB extends Threads { private Signal signal; public TrainB(Signal signal){
this.signal = signal;
System.out.println("TrainB : Run Engine");
}

@Override
public void run() {
if(nearby_station){
System.out.println("Waiting for trainA to be passed");
signal.wait();
}
System.out.println("TrainB : Pass from the station");
Thread.sleep(5*60*1000);
System.out.println("TrainB : Passed from the station");
}
}

What is this signal here?
For now, let’s consider it as when Train B calls signal.wait() it shuts down its engine and when Train A calls signal.notify() a green light stuck to its end becomes active implying Train B can pass now.

But, on the night of 28th March, Train A was earlier by 10 minutes and it reached the station at 9.50 pm and left by 10.00 pm. Now Train B came and waited for Train A to come and pass and show up its favorite green light.

But it didn’t appear till 10.15 pm.

Now Train B’s crew have two choices

  1. Wait for more and listen to the angry passengers. And they even don’t know how much they need to wait.
  2. Second, move towards the station and consider the possibility of a crash with Train A. (Remember that Train A couldn’t check for other trains at the station due to the sharp turn)

This is what we call a MISSED SIGNAL!

So, what would you do?

Replace the signaling algorithm right. What if instead of Train A showing the go-go light itself, it gives this responsibility to the station manager. If Train A has already passed the station manager at the station would tell the Train B crew to move on.

How do we handle this in code? Another flag that Train A has already passed or not.

if(nearby_station && !station_manager_green_flag_present){
System.out.println("Waiting for trainA to be passed");
signal.wait();
}

So they did it!

https://data.whicdn.com/images/31887220/original.gif

Now consider another unfortunate situation and forget about the previous situation what if the control system of Train B fails and it starts to run automatically while waiting near the station for Train A to pass.

Disaster Right!

This is equivalent to the situations where one can get into during SPURIOUS WAKEUPS in the JAVA thread management system.

What does this mean?

if(nearby_station && !station_manager_green_flag_present){
System.out.println("Waiting for trainA to be passed");
signal.wait();
}

This signal.wait() instruction on Train B’s run method is supposed to pause the Train B thread while the notify() signal is not given to it. But sometimes, for very rare cases it fails to do so. Thus called spurious wakeups.

How to solve this?

Just change the if condition with a while loop. So when it wakes up without any notify signal, it rechecks the condition, and if the station manager still didn’t give any green flag, it goes back to sleep.

while(nearby_station && !station_manager_green_flag_present){
System.out.println("Waiting for trainA to be passed");
signal.wait();
}

# Notice
In our software scenario, the station manager needs to be ready with the green flag when Train A notifies Train B, otherwise, Train B would be stuck in the while loop forever.

You would be probably wondering why didn’t we hired the station manager in the first place to make the process simple like

When Train A is ready to leave, the Station manager informs Train B to start the engine and be ready for the next signal from him to enter. Or in the case when Train-B reaches the station first, it contacts the station manager whether Train A is on the way to enter the station or not.

But you should see there is a very high responsibility on the station manager otherwise the complexity of the Signal class in that case

…. And with 100 such crossings, how difficult it would be to handle.

https://media.giphy.com/media/pntEStkSm6orC/giphy.gif

But on the other hand, you should acknowledge that how the first approach makes the Station Manager job simple as he just needs to put a flag and get back on his delicious sandwich and we are also making all the systems somewhat decoupled.

Yeah, Trains talking to each other via signal. Cool isn’t it.

We only needed the station manager to handle unfortunate situations like Missed Signals and Spurious Wakeups by having just a single simple green flag.

So, guys, that’s all on Thread Signalling. To summarize, this is what we learned today.

  1. How to achieve immutability
  2. How to remove busy waiting
  3. How to handle missed signals
  4. How to handle spurious wakeups.

Thanks for reading this throughout and I am planning to continue writing on this topic and the next section would deal with Deadlock and more.

Stay tuned !!

--

--