Nearby lessons

143 of 159

Python - RLock (Reentrant Lock)

📌 What You Will Learn
  • Explain the problem with a simple Lock
  • Understand why recursive functions need RLock
  • Know that RLock is reentrant for the owning thread
  • Track the matching acquire() and release() count
  • Analyze the recursive factorial example

Problem with Simple Lock

A normal Lock has an important limitation.

The standard Lock object does not care which Thread is currently holding that Lock.

If the Lock is already held and any Thread attempts to acquire the same Lock again, that Thread becomes blocked.

This rule applies even when the Thread trying to acquire the Lock again is the same Thread that already owns it.

Simple Definition:

A normal Lock cannot be acquired again by the same Thread while that Lock is already held.

Main Thread
     │
     ▼
Acquire Lock
     │
     ▼
Lock is Held
     │
     ▼
Main Thread Tries
to Acquire Same Lock
Again
     │
     ▼
Blocked

Demonstration Program - Problem with Simple Lock

🐍Code Cell
1from threading import *
2 
3l = Lock()
4 
5print("Main Thread trying to acquire Lock")
6l.acquire()
7 
8print("Main Thread trying to acquire Lock Again")
9l.acquire()
Output
No output captured.

Output

What Happens After This Output?

After printing:

Main Thread trying to acquire Lock Again

the program does not continue.

The Main Thread becomes blocked because it is trying to acquire the same normal Lock for the second time.

First acquire()
      │
      ▼
Success
      │
      ▼
Lock Already Held
by Main Thread
      │
      ▼
Second acquire()
      │
      ▼
Main Thread Waits
      │
      ▼
No Thread Releases Lock
      │
      ▼
Program Remains Blocked

Program Explanation

Step 1: Import threading

from threading import *

This imports the required classes and functions from the threading module.


Step 2: Create a Lock Object

l = Lock()

A normal Lock object is created.

Initially, this Lock is available.


Step 3: Print the First Message

print("Main Thread trying to acquire Lock")

The Main Thread displays a message before acquiring the Lock.


Step 4: Acquire the Lock

l.acquire()

The Lock is currently available.

Therefore, the Main Thread successfully acquires it.

Lock Available
     │
     ▼
Main Thread
l.acquire()
     │
     ▼
Lock Acquired

Step 5: Print the Second Message

print("Main Thread trying to acquire Lock Again")

This message is displayed normally.

At this point, the Main Thread still owns the Lock.


Step 6: Acquire the Same Lock Again

l.acquire()

Now the problem occurs.

The Lock is already held.

A normal Lock does not provide a reentrant facility.

Therefore, the Main Thread becomes blocked.

Why Does the Program Stop?

The Main Thread acquired the Lock once:

l.acquire()

Without releasing it, the same Main Thread again executes:

l.acquire()

Because a standard Lock is already in the locked state, the second acquire() waits for the Lock to become available.

But the same Thread is waiting and cannot continue to a future release().

Main Thread
    │
    ▼
Acquire Lock
    │
    ▼
Lock Held
    │
    ▼
Acquire Again
    │
    ▼
Wait for Lock
    │
    ▼
Same Thread Cannot
Continue
    │
    ▼
Blocked

Important Note - Stopping the Blocked Program

The document gives an important command-line note.

To terminate the blocking Thread from the Windows command prompt, use:

Ctrl + Break

The document specifically notes that:

Ctrl + C

does not work for this example in the described environment.

Another Problem with Simple Lock

The problem becomes more important when a Thread works with:

  • Recursive functions
  • Nested access to resources

In such cases, the same Thread may need to acquire the same Lock multiple times.

Thread
  │
  ▼
Function A
  │
  ▼
Acquire Lock
  │
  ▼
Function B
  │
  ▼
Needs Same Lock
  │
  ▼
Acquire Again
  │
  ▼
Blocked with
Normal Lock

Problem with Recursive Functions

A recursive function calls itself.

Suppose a recursive function contains:

l.acquire()

Every recursive call can attempt to acquire the same Lock again.

factorial(3)
     │
     ▼
Acquire Lock
     │
     ▼
factorial(2)
     │
     ▼
Acquire Same Lock Again
     │
     ▼
Blocked with Lock

Therefore, a traditional Lock is not suitable when the same Thread must repeatedly acquire the same Lock during recursion.

Limitation of Traditional Locking

The document concludes:

Traditional Locking mechanism won't work for executing recursive functions.

The reason is simple.

Normal Lock
    │
    ▼
First acquire()
    │
    ▼
Success
    │
    ▼
Same Thread Calls
acquire() Again
    │
    ▼
Blocked

Therefore, we need a synchronization mechanism that allows the owner Thread to acquire the same Lock multiple times.

Solution - RLock

To overcome the limitation of a normal Lock, Python provides:

RLock

RLock means:

Reentrant Lock

Simple Definition:

RLock allows the owner Thread to acquire the same Lock again and again.

If another Thread tries to acquire the RLock while it is held, that other Thread must wait.

What Does Reentrant Mean?

Reentrant means that the Thread which already owns the Lock can enter the same protected locking mechanism again.

Owner Thread
     │
     ▼
Acquire RLock
     │
     ▼
Acquire Same RLock Again
     │
     ▼
Allowed
     │
     ▼
Acquire Again
     │
     ▼
Allowed

This is the main difference between Lock and RLock.

Reentrant Facility

The reentrant facility is available only for the owner Thread.

It is not available to other Threads.

                 RLock
                   │
          Owner = Thread-1
                   │
        ┌──────────┴──────────┐
        │                     │
        ▼                     ▼
Thread-1 acquire()       Thread-2 acquire()
Again                     Same RLock
        │                     │
        ▼                     ▼
     Allowed                Wait

Therefore:

  • The owner Thread can acquire the same RLock multiple times.
  • Another Thread must wait until the RLock is completely released.

Creating an RLock Object

An RLock object can be created using:

l = RLock()

Complete syntax:

from threading import *

l = RLock()

Here:

  • RLock() creates a Reentrant Lock.
  • l refers to that RLock object.

RLock Example

🐍Code Cell
1from threading import *
2 
3l = RLock()
4 
5print("Main Thread trying to acquire Lock")
6l.acquire()
7 
8print("Main Thread trying to acquire Lock Again")
9l.acquire()
Output
No output captured.

Output

Important Observation

Unlike the previous normal Lock example, the Main Thread does not become blocked on the second acquire().

This is because:

l = RLock()

is used instead of:

l = Lock()

The same owner Thread can acquire an RLock multiple times.

RLock Program Explanation

Step 1: Import threading

from threading import *

The threading functionality is imported.


Step 2: Create RLock

l = RLock()

An RLock or Reentrant Lock object is created.


Step 3: Print the First Message

print("Main Thread trying to acquire Lock")

The Main Thread announces that it is going to acquire the Lock.


Step 4: Acquire RLock

l.acquire()

The Main Thread successfully acquires the RLock.


Step 5: Print the Second Message

print("Main Thread trying to acquire Lock Again")

The Main Thread is about to acquire the same RLock again.


Step 6: Acquire RLock Again

l.acquire()

The same Main Thread already owns the RLock.

Because RLock is reentrant, the second acquisition is allowed.

Therefore, the Main Thread is not blocked.

Lock vs RLock - Same Program

Using Lock

l = Lock()

l.acquire()
l.acquire()

Result:

Second acquire()
      │
      ▼
Blocked

Using RLock

l = RLock()

l.acquire()
l.acquire()

Result:

Second acquire()
      │
      ▼
Allowed for
Owner Thread

Recursion Level in RLock

RLock keeps track of the recursion level.

Every time the owner Thread calls:

l.acquire()

the recursion level increases.

Every time it calls:

l.release()

the recursion level decreases.

The RLock becomes completely available to another Thread only after the required matching release() calls are executed.

Initial Level = 0

acquire()
    │
    ▼
Level = 1

acquire()
    │
    ▼
Level = 2

release()
    │
    ▼
Level = 1

release()
    │
    ▼
Level = 0
    │
    ▼
RLock Completely Released

Matching acquire() and release() Calls

For every acquire() call, a corresponding release() call should be available.

The number of acquisitions and releases should match before the RLock becomes completely released.

Example:

l = RLock()

l.acquire()
l.acquire()

l.release()
l.release()

Here:

Operation Recursion Level
Initial 0
First acquire() 1
Second acquire() 2
First release() 1
Second release() 0

Only after the second release() is the RLock completely released.

Important Notes About Recursion Level

  1. Only the owner Thread can acquire the same RLock multiple times.
  2. The number of acquire() and release() calls should match.
  3. RLock internally keeps track of the recursion level.

Why RLock is Useful for Recursive Functions

Consider a recursive function:

factorial(5)
   │
   ▼
factorial(4)
   │
   ▼
factorial(3)
   │
   ▼
factorial(2)
   │
   ▼
factorial(1)
   │
   ▼
factorial(0)

The same Thread executes all these recursive calls.

If every recursive call needs the same synchronization Lock, a normal Lock creates a problem.

RLock solves this because the same owner Thread is allowed to acquire the same RLock repeatedly.

Demo Program - Synchronization Using RLock

🐍Code Cell
1from threading import *
2import time
3 
4l = RLock()
5 
6def factorial(n):
7 l.acquire()
8 
9 if n == 0:
10 result = 1
11 else:
12 result = n * factorial(n - 1)
13 
14 l.release()
15 return result
16 
17def results(n):
18 print("The Factorial of", n, "is:", factorial(n))
19 
20t1 = Thread(target=results, args=(5,))
21t2 = Thread(target=results, args=(9,))
22 
23t1.start()
24t2.start()
Output
No output captured.

Output

Complete Program Explanation

Step 1: Import Modules

from threading import *
import time

The required threading functionality is imported.


Step 2: Create RLock

l = RLock()

A single RLock object is created.

This RLock is shared during recursive function execution.


Step 3: Define factorial()

def factorial(n):

The factorial() function calculates the factorial of a number recursively.


Step 4: Acquire RLock

l.acquire()

Every call to factorial() attempts to acquire the RLock.


Step 5: Check Base Condition

if n == 0:
    result = 1

When n becomes 0, recursion stops.

The factorial of zero is:

0! = 1

Step 6: Recursive Call

result = n * factorial(n - 1)

The function calls itself.

Because the same Thread executes the recursive call, it attempts to acquire the same RLock again.

RLock permits this.


Step 7: Release RLock

l.release()

Every recursive invocation releases one acquisition of the RLock before returning.

Therefore, every acquire() has a matching release().


Step 8: Return Result

return result

The calculated factorial value is returned.


Step 9: Define results()

def results(n):
    print("The Factorial of", n, "is:", factorial(n))

This function calls factorial() and displays the result.


Step 10: Create Two Threads

t1 = Thread(target=results, args=(5,))
t2 = Thread(target=results, args=(9,))

Two Threads are created.

Thread Function Value
t1 results() 5
t2 results() 9

Step 11: Start Both Threads

t1.start()
t2.start()

Both Threads start executing.

When one Thread owns the RLock, another Thread requesting it must wait.

However, recursive calls made by the owner Thread can acquire the same RLock repeatedly.

How factorial(5) Uses RLock

Conceptually, the recursive acquisition works like this:

factorial(5)
Acquire → Level 1
      │
      ▼
factorial(4)
Acquire → Level 2
      │
      ▼
factorial(3)
Acquire → Level 3
      │
      ▼
factorial(2)
Acquire → Level 4
      │
      ▼
factorial(1)
Acquire → Level 5
      │
      ▼
factorial(0)
Acquire → Level 6
      │
      ▼
Base Case
result = 1

While returning:

factorial(0)
Release → Level 5
      │
      ▼
factorial(1)
Release → Level 4
      │
      ▼
factorial(2)
Release → Level 3
      │
      ▼
factorial(3)
Release → Level 2
      │
      ▼
factorial(4)
Release → Level 1
      │
      ▼
factorial(5)
Release → Level 0
      │
      ▼
RLock Completely Released

Factorial Calculation

For factorial(5):

factorial(5)
= 5 × factorial(4)
= 5 × 4 × factorial(3)
= 5 × 4 × 3 × factorial(2)
= 5 × 4 × 3 × 2 × factorial(1)
= 5 × 4 × 3 × 2 × 1 × factorial(0)
= 5 × 4 × 3 × 2 × 1 × 1
= 120

Therefore:

The Factorial of 5 is: 120

Similarly:

9! = 362880

Important Observation - What if We Use Lock?

The document gives a very important observation:

If we use a normal Lock instead of RLock in this program, the Thread will be blocked.

Suppose we change:

l = RLock()

to:

l = Lock()

Then:

factorial(5)
     │
     ▼
Acquire Lock
     │
     ▼
factorial(4)
     │
     ▼
Same Thread Attempts
to Acquire Same Lock
     │
     ▼
Blocked

This demonstrates why RLock is required for this recursive program.

Execution Flow - RLock Factorial Program

Program Starts
      │
      ▼
Create RLock
      │
      ▼
Create Threads
t1 and t2
      │
      ▼
Start Threads
      │
      ▼
One Thread Acquires
RLock
      │
      ▼
factorial(n)
      │
      ▼
Recursive Function Calls
      │
      ▼
Same Owner Thread
Acquires RLock Again
      │
      ▼
Recursion Continues
      │
      ▼
Base Condition
n == 0
      │
      ▼
Recursive Calls Return
      │
      ▼
Matching release()
Calls Execute
      │
      ▼
Recursion Level
Becomes 0
      │
      ▼
RLock Completely Released
      │
      ▼
Waiting Thread Can
Acquire RLock
      │
      ▼
All Threads Complete
      │
      ▼
Program Ends

Problem with Lock vs Solution with RLock

Situation Lock RLock
First acquisition by Thread Allowed Allowed
Same owner Thread acquires again Blocked Allowed
Different Thread tries while held Blocked Blocked
Recursive function Not suitable Suitable
Nested resource access Not suitable Suitable
Tracks owner Thread No Yes
Tracks recursion level No Yes

Difference Between Lock and RLock

Lock RLock
A Lock object can be acquired by only one Thread at a time. Even the owner Thread cannot acquire the same Lock multiple times. An RLock object can be acquired by only one Thread at a time, but the owner Thread can acquire the same RLock multiple times.
Not suitable for recursive functions and nested access calls. Best suitable for recursive functions and nested access calls.
Lock takes care of whether it is locked or unlocked. RLock takes care of whether it is locked or unlocked and also maintains owner Thread information and recursion level.

Lock and RLock Visual Comparison

Normal Lock

Thread-1
   │
   ▼
acquire()
   │
   ▼
Success
   │
   ▼
acquire() Again
   │
   ▼
BLOCKED

RLock

Thread-1
   │
   ▼
acquire()
   │
   ▼
Success
Level = 1
   │
   ▼
acquire() Again
   │
   ▼
Success
Level = 2
   │
   ▼
release()
Level = 1
   │
   ▼
release()
Level = 0
   │
   ▼
RLock Released

When Should We Use RLock?

RLock is particularly useful when:

  • A recursive function requires synchronization.
  • A Thread may enter the same protected code multiple times.
  • Functions call other synchronized functions that use the same Lock.
  • Nested resource access requires repeated acquisition by the same Thread.
Recursive / Nested Access
          │
          ▼
Same Thread May Need
Same Lock Again
          │
          ▼
Use RLock

Summary

  • A normal Lock cannot be acquired repeatedly by the same owner Thread while it remains locked.
  • If the owner Thread attempts to acquire the normal Lock again, it becomes blocked.
  • This creates problems with recursive functions and nested resource access.
  • Traditional Locking is therefore not suitable for such recursive locking requirements.
  • Python provides RLock to solve this problem.
  • RLock stands for Reentrant Lock.
  • The owner Thread can acquire the same RLock multiple times.
  • Other Threads must wait while the RLock is held.
  • RLock maintains the recursion level.
  • Every acquire() must have a corresponding release().
  • The RLock becomes completely released when the recursion level returns to zero.
  • RLock is suitable for recursive functions and nested resource access.

Important Notes

  1. A standard Lock allows only one acquisition while it remains locked.
  2. Even the owner Thread cannot acquire the same normal Lock again.
  3. Acquiring the same normal Lock twice causes the Thread to become blocked.
  4. The document notes using Ctrl + Break to terminate the blocked Thread from the Windows command prompt in its example environment.
  5. Recursive functions may attempt to acquire the same Lock multiple times.
  6. Nested resource access can also require repeated acquisition of the same Lock.
  7. Traditional Lock is not suitable for such cases.
  8. RLock means Reentrant Lock.
  9. The owner Thread can acquire the same RLock multiple times.
  10. The reentrant facility is available only to the owner Thread.
  11. Other Threads remain blocked while another Thread owns the RLock.
  12. RLock keeps track of the recursion level.
  13. Every acquire() call should have a matching release() call.
  14. For two acquire() calls, two corresponding release() calls are required to completely release the RLock.
  15. RLock is suitable for recursive functions.
  16. RLock is also suitable for nested resource access.
  17. In the factorial example, recursive calls made by the same Thread repeatedly acquire the same RLock.
  18. If a normal Lock is used instead of RLock in the factorial program, the Thread becomes blocked.
📝 Key Takeaways
  • A normal Lock blocks the owning thread if it tries to acquire again
  • RLock allows the same thread to acquire it multiple times
  • Every acquire() on an RLock needs a matching release()
  • RLock is the right choice for recursive functions

🧠 Test Your Knowledge

15 Questions
Progress: 0 / 15