Hello and welcome!
In this video session we will begin a new topic on thread synchronization.
We will understand what is a thread synchronization and why it is required, and what are the different
methods that can be used to achieve thread synchronization.
So some of the methods of thread synchronization we will be studying are mutex or mutual exclusion and
condition variables which work on wait and signal method.
Now let us see the synchronization in threads.
So as we have seen the basics of thread that multiple threads can execute in a process and the threads
share the global variables, that means the thread has access to the data segment, the heap and the
heap segment.
So the data that are defined in these memory segment can be accessed across the threads.
So in this case, whenever the thread.
Tries to access these resources, we have to take care of the synchronization between the threads.
Otherwise, you know inappropriate results can occur.
So we have seen in the previous video sessions that in order to achieve synchronization, we have to
write the code in a critical section manner.
And we have to maintain the atomicity.
So we can say that a part or a piece of code is in the critical section when its execution is not interrupted
by other thread execution.
Now let us see some of the incorrect method of multi-threaded programming where synchronization issues
can occur.
Let us see this with an example
Now let's see the incorrect method of using global variables and which can cause a potential synchronization
issues.
So in this program we include the required header files.
And we have a global variable uh glob.
This can also be considered as a shared resource because this variable is can be shared across the different
threads here.
Let's begin with the main thread here.
So the main function can be considered as a main thread.
And we declare t1 and t2 of type p thread underscore t and we create two threads here pthreadcreate
passing t1 and similarly create pthreadcreate passing t2 and we specify thread func one for t1 and thread
func two for t2, and then later the main thread waits for t1 and t2 complete by calling p thread join
and then finally the main thread exits.
Now let's go to the third definition.
We have Thread funk one and Thread Funk two.
In this the each thread it for it executes for a loop for 0 to 10 10,000 as specified here.
So what happens in the loop is it takes the global global variable to the local variable variable here.
And whatever is the global value it is copied to the local variable.
And it increments the local variable and puts back the value to the global variable.
Similar thing is also done in thread funk two.
The loop runs for 0 to 10,000, and it takes the value from global and stores to the local variable,
and increments the local variable and puts back the local variable to global.
And then once the loop is finished, it returns.
So now what we can expect is, uh.
What, uh, what, as a user, as a programmer, we expect this.
So let's say the global count is zero.
So when it runs for 10,000 times, uh, this thread and this thread runs for 10,000 times.
So at the end of the execution, we will be expecting that the global value will be 20,000.
So let's compile this and see the output.
Let's create the output file here.
As mentioned we should we should link with the thread library.
Now we have the output file.
Let's execute this.
Okay.
First execution we got 20,000.
Second execution we got 13,999.
Again we got 20,000 15,008 46.
So in different executions We are getting different values.
So this means the output.
What we are getting is not, uh, we are not getting the same output in every run.
This means there is a synchronization issue here.
If we note here, every time the global variable value is different, sometimes it gives the correct
value, sometimes it gives a different value.
But this means that the the code here is not running in a atomic manner.
So we have to whenever we access the global variable we have to.
For example, in in this code we can say that these three lines are the critical section.
And this should be accessed in an atomic manner.
This means when the thread enters executing this line, this should not be interrupted by any other
thread.
For example, let's say that what is happening is let's say that first thread has copied the value to
location here when it is about to execute this line.
If this thread is scheduled, then the global value will be copied.
Here is the old global value.
It will not be the one which is same as here.
So then it increments.
Then it puts back the global value value here.
So now the global value has different value than what is stored in the local value here.
Hence there is an synchronization issue in this program.
Now let's go back to our presentation.
So here we see that this is an example in which the it is an incorrect way of multi-threaded programming
which can cause the synchronization issue while accessing the global resource.
This is what we have seen.
So now let's see what are the different methods of, uh, writing the code in a synchronization fashion.
So one of the method used to synchronize between the threads is mutex or mutex.
Mutex is nothing, but it's a type of a lock and its data type is p pthread mutex underscore t.
So a mutex helps in running the critical section in an atomic manner.
The mutex has basically two states that is a locked state and an unlocked state.
Now one more important thing about the mutex is only the thread which has locked becomes the owner,
and the owner thread only can unlock the mutex.
So now let's see the operations of the mutex.
So the thread that needs to access the critical section, it first needs to lock the mutex and then
access the shared resource and then unlock the mutex.
So this three steps will help to gain the synchronization between multi-threaded programming that needs
to access the common resource or the shared resource.
Now let's see how this works here.
So now let's consider we have two threads.
Thread A and thread B running in a multi-threaded fashion.
Now we are not sure which thread will be um, which thread runs at what time.
Or we can say that we are not sure when the thread will be scheduled.
Now let's say that if thread A is scheduled first and now it locks using the mutex M, and now after
locking, if the lock is successful, it now proceeds to access the global variable.
Now let's say when the thread A is accessing the global resource.
Now if thread B is scheduled now similarly it tries to lock the mutex M the same mutex.
What is used here when it tries to lock the mutex.
Since the mutex is already locked this cannot lock the mutex once again.
So thread b blocks on the mutex m and the lock is not granted and it keeps on waiting in a block state
or in a sleep state until it gets access to the mutex.
Now here the thread A finishes the accessing or the modification of the global resource and then unlocks
the mutex.
And once the mutex is unlocked now the thread B Which had tried to lock the mutex will now get unblocked,
and now the lock is granted for thread B and the lock owner is now thread B.
Whoever gets the lock or grant gets the grant to lock will be the owner of that mutex.
Now in this case thread B is the owner.
And further it tries to access the resource.
And after accessing or modifying the resource now it unlocks.
So now we can make sure that this global variable is protected by the locks.
And now this section of code from here to here runs in a atomic manner.
And this is a critical section.
So the section of the code in which the common resources are accessed can be considered as the critical
section.
And in order to achieve the atomicity in this case we can use the logs so that synchronization can be
achieved.
Now let's see the programming program concepts of mutex.
So in this case we will discuss the statically allocated mutex.
So we have two types of mutex basically static allocated and dynamically dynamic mutex.
So here we will be considering only the static mutex pthread mutex underscore T is the data type of
the mutex.
And let's call the variable name as int x.
So we need to initialize the mutex p thread underscore mutex underscore initialize.
So this initializer will tell that MT is a is is a type mutex that is statically allocated.
Now let's see the syntax of the mutex.
So in order to use the mutex we need to include the header file pthread dot h.
And as we have seen in this example, that we after declaring a mutex, we are performing lock operation
and a unlock operation on the mutex.
So we have pthread mutex lock and pthread mutex unlock on the mutex variable.
So this this is a mutex variable.
And we need to lock on this mutex variable and unlock the mutex variable.
So both this function call returns a zero on success.
If there is an error it has a positive number.
It returns a positive number on error.
Also let's see what is a deadlock.
Now consider two threads running thread A and thread b.
Now let's say that thread A needs two mutex locks.
That is log A and log B in order to access the shared resource.
Similarly, thread B also needs lock A and lock B in order to access the shared resource.
So here in this case, thread locks the mutex variable mutex lock a and then it is about to put a mutex
lock on b mutex lock b.
Meanwhile, if thread b has already put a lock on mutex lock B.
Now thread A waits until the mutex lock B is released.
Now this thread A is waiting for thread B to release its mutex lock B, whereas in the same way thread
B now goes further and tries to lock mutex lock A, but mutex lock A is already locked by thread A.
So thread A has got mutex lock A and thread b has got mutex lock in the.
When we say got mutex lock, that means the thread a has a.
Put a lock on mutex, lock a and thread B has put a lock on mutex lock B.
Now when thread A is executing this, it will block because this lock is already locked by the other
thread.
And this lock that is a mutex lock A is already locked by thread A, so both the threads are waiting
for each other to complete.
Now this is called as a deadlock where it keeps on waiting indefinitely for the thread that is acquired
by the other thread to be released.
Now let us see what are the methods or what we need to follow in order to avoid the deadlock.
So in order to avoid the deadlock, we need to define the mutex in a hierarchical fashion.
This means we need to try to have a same sequence of locking on the mutex.
So for example, let's say that in the previous uh deadlock situation we saw thread A is putting a lock
on A.
Then on B thread B is putting a lock on B, then on A.
Instead of this, if we follow the other manner.
For example, thread A puts a lock on A, then on b same in the same way thread B puts a lock on a,
then on, then the then puts a lock on b.
Now this is a a locking in a hierarchical manner.
So now let's say thread a has gained to put a lock on mutex lock A.
Now if thread B also tries to put a lock here thread B blocks because mutex lock A is already locked
by other thread.
Now thread A can proceed further and put a lock on mutex lock B as a result, thread A can access both
mutex lock and mutex lock B and perform the operation on shared resource and then unlock mutex lock
B and mutex lock A, then the thread B which was blocked.
For mutex lock A can acquire the lock both mutex lock A and B and perform the operation on shared resource
and then unlock both the locks.
So by this, by following an hierarchical fashion of locking the mutex, the deadlocks can be avoided.
Now this is uh this is uh the function call similar to mutex lock.
But it, it has a slightly small different variations than the uh pthread mutex lock.
So we have pthread mutex trylock and thread mutex time lock.
So the pthread mutex trylock is one in which it tries to put a lock.
And at that moment if the lock is being locked, if the mutex is being locked by some other thread,
it does not block the function.
Instead it returns.
It returns with a error saying that it returns with code ebusy.
Similarly, the function pthread mutex time lock.
This is also same to similar to pthread mutex lock, but in this case we can specify the time in which
it can get blocked.
So when a thread calls pthread mutex time lock with some time parameter.
Here it tries to put a lock on the specified mutex variable.
If that mutex is already been locked by some other thread, it waits for the duration that is specified
in this function call, and even after that duration.
If that lock is not available, then it returns to the calling thread with the error code E timeout.
Now let's see some of the examples of pthread mutex lock.
In this program we will see the thread mutex and this can be used for synchronization.
This program is similar to the previous program.
But in this case we have taken care of um achieving the synchronization.
So here we have a global variable.
Also we are defining a new mutex variable MT is the mutex variable of type p thread mutex underscore
t.
And we have initialized with mutex initializes initializer.
So this is a static mutex.
Now let's go to the main thread.
Here the main thread begins with defining two threads, t1 and t2 of type p thread underscore t, and
it creates both the thread and calls the function.
Thread func one and thread func two, which is defined here.
Thread func one and thread func two.
And then.
Further.
The main thread calls a join to both t1 and t2 thread.
Thus, the main thread waits until both the thread that is created has exited.
Now let's go to the definition of both the threads.
If you see the difference between the previous program and this program, we can see that before performing
the operation on the global variable, we are putting a mutex lock.
And then we are performing the access to the global variable.
And then we are unlocking the mutex.
So MDX is the global Mutex that is used similarly in thread function two.
Also, this is the critical section that means this is the piece of the code that accesses the global
variable globe.
And before accessing that we are putting a lock.
Same lock that is that is the global.
That is the variable MT.
And then unlocking by the same mutex variable.
So now what happens is as soon as whichever thread accesses the lock first let's say that function one
has got the lock.
So once this lock is put here.
Now if the other thread tries to put a lock here, now this thread is blocked.
That means it cannot further execute the other code's other part of the code.
And it it waits in this line only until the lock is been released by the other thread here.
Now let's run this program to understand the behavior.
Now let's compile the program.
Now let's run the program.
We can see that the global value is 20,000.
So any number of time we execute the global variable the global variable value will be 20,000.
So we are seeing that the global value is always 20,000.
But in the previous but in the previous program we saw that the global value is not 20,000.
This is this.
This was one underscore.
It was the incorrect way of multi-threaded programming where there was a synchronous.
Synchronization issues.
So in by using mutex lock we are having a proper way of synchronization between the threads.
So in this case anytime we run we are getting 20,000 only.
Now let's see one more example.
This program is similar to the previous program.
But one thing what is different here is here.
Say in a similar manner we are creating two threads t1 and T2.
But if we note here both the definition of the thread are same here.
So the text segment of both the threads are same.
We are not using two separate functions for thread one and thread two.
Instead, we are having the same function that is shared between thread one and thread two.
to.
So this behavior is also similar to the previous code.
So this is more efficient way of writing the code where we are trying to put all the common code of
thread function in one function.
Let's compile this as well and see the output.
Now let's run this.
You can see that the output of the global variable is always 20,000.
That means each thread has incremented the global variable by 10,000.
Right now we have one main thread and two threads running that is t1 and T2.
Both share the same text segment code