Nearby lessons

142 of 159

Python - Synchronization Using Lock

📌 What You Will Learn
  • Create a Lock object with threading.Lock()
  • Use acquire() and release() correctly
  • Understand that only one thread can hold a lock at a time
  • Explain the release() RuntimeError on an unlocked lock
  • Analyze the synchronized wish() example

Synchronization Using Lock

Python provides different synchronization mechanisms.

One of the most fundamental synchronization mechanisms provided by the threading module is:

Lock

A Lock is used to make sure that only one Thread can access a particular critical section at a time.

Simple Definition:

A Lock allows only one Thread at a time to execute the protected code.

Multiple Threads
      │
      ▼
     Lock
      │
      ▼
Only One Thread
Gets Permission
      │
      ▼
Critical Section

Why Do We Need Lock?

Without synchronization, multiple Threads may execute the same shared code simultaneously.

This can result in:

  • Irregular output
  • Data inconsistency

A Lock controls access to the shared resource.

Without Lock

Thread-1 ───┐
Thread-2 ───┼──► Shared Resource
Thread-3 ───┘

Possible overlapping execution


With Lock

Thread-1 ───► Lock ───► Shared Resource

Thread-2 ───► Wait

Thread-3 ───► Wait

Creating a Lock Object

We can create a Lock object as follows:

l = Lock()

For this, Lock is imported from the threading module.

from threading import *

l = Lock()

Here:

  • Lock() creates a Lock object.
  • l is the reference variable pointing to the Lock object.
  • The same Lock can be shared by multiple Threads.

How Does a Lock Work?

The Lock object can be held by only one Thread at a time.

If another Thread requires the same Lock, it must wait until the current Thread releases the Lock.

Thread-1
   │
   ▼
Acquire Lock
   │
   ▼
Lock Occupied
   │
   ├──────────── Thread-2 → Wait
   │
   └──────────── Thread-3 → Wait
   │
   ▼
Thread-1 Executes
   │
   ▼
Release Lock
   │
   ▼
One Waiting Thread
Gets the Lock

Real-Life Comparison

The document compares a Lock with examples such as:

  • Common wash rooms
  • Public telephone booths

Consider a public telephone booth.

Person-1
   │
   ▼
Using Booth
   │
   ├──── Person-2 → Wait
   ├──── Person-3 → Wait
   │
   ▼
Person-1 Leaves
   │
   ▼
Next Person Enters

Only one person can use the resource at a time.

A Thread Lock works using the same basic idea.

acquire() Method

A Thread can acquire the Lock by using the acquire() method.

Syntax:

l.acquire()

When a Thread successfully executes:

l.acquire()

that Thread gets the Lock.

Thread
  │
  ▼
l.acquire()
  │
  ▼
Is Lock Available?
  │
  ├──── Yes ───► Acquire Lock
  │
  └──── No ─────► Wait

What Happens if the Lock is Already Acquired?

Suppose Thread-1 already holds the Lock.

Thread-1
   │
   ▼
l.acquire()
   │
   ▼
Lock Acquired

Now Thread-2 executes:

l.acquire()

But the Lock is not available.

Therefore:

Thread-2
   │
   ▼
l.acquire()
   │
   ▼
Lock Already Held
by Thread-1
   │
   ▼
Thread-2 Waits

Thread-2 can continue only after the Lock becomes available.

release() Method

A Thread can release the Lock by using the release() method.

Syntax:

l.release()

Once the Lock is released, another waiting Thread can acquire it.

Thread-1
   │
   ▼
Critical Section
   │
   ▼
l.release()
   │
   ▼
Lock Available
   │
   ▼
Another Waiting Thread
Can Acquire Lock

acquire() and release()

The normal Lock execution pattern is:

l.acquire()

# Critical Section
# Shared Resource Operations

l.release()

The sequence is:

Acquire
   │
   ▼
Execute Protected Code
   │
   ▼
Release
Method Purpose
acquire() Acquire the Lock
release() Release the Lock

Important Rule of release()

The document gives an important rule:

To call release(), the Lock must already be in the acquired state.

If we try to release an unlocked Lock, Python raises:

RuntimeError: release unlocked lock

Therefore, the normal sequence should be:

l.acquire()
     │
     ▼
Lock Acquired
     │
     ▼
l.release()
     │
     ▼
Lock Released

Not:

Lock Not Acquired
      │
      ▼
l.release()
      │
      ▼
RuntimeError

Example - release() Without acquire()

🐍Code Cell
1from threading import *
2 
3l = Lock()
4 
5# l.acquire() # Line-1
6 
7l.release()
Output
No output captured.

Output When Line-1 is Commented

Program Explanation - release() Error

Step 1: Import threading

from threading import *

The required Threading classes and functions are imported.


Step 2: Create the Lock

l = Lock()

A new Lock object is created.

Initially, the Lock is not acquired.


Step 3: acquire() is Commented

# l.acquire()

Because this statement is commented, the Lock remains unlocked.


Step 4: Call release()

l.release()

Python is asked to release a Lock that is currently unlocked.

Therefore, Python raises:

RuntimeError: release unlocked lock

Execution Flow - release unlocked lock

Program Starts
      │
      ▼
Create Lock
      │
      ▼
Lock is Unlocked
      │
      ▼
l.acquire()
Commented
      │
      ▼
Execute
l.release()
      │
      ▼
Trying to Release
Unlocked Lock
      │
      ▼
RuntimeError:
release unlocked lock

What Happens if Line-1 is Not Commented?

If we execute:

l.acquire()

before:

l.release()

the sequence becomes valid.

from threading import *

l = Lock()

l.acquire()
l.release()

Execution:

Create Lock
    │
    ▼
Acquire Lock
    │
    ▼
Lock Becomes Locked
    │
    ▼
Release Lock
    │
    ▼
Lock Becomes Unlocked
    │
    ▼
Program Ends Normally

Synchronization Example Using Lock

Now the document applies Lock to the earlier wish() synchronization problem.

Three Threads are created:

  • Dhoni
  • Yuvraj
  • Kohli

All three Threads execute the same wish() function.

But before entering the protected code, each Thread must acquire the same Lock.

Complete Program - Synchronization Using Lock

🐍Code Cell
1from threading import *
2import time
3 
4l = Lock()
5 
6def wish(name):
7 l.acquire()
8 
9 for i in range(10):
10 print("Good Evening:", end='')
11 time.sleep(2)
12 print(name)
13 
14 l.release()
15 
16t1 = Thread(target=wish, args=("Dhoni",))
17t2 = Thread(target=wish, args=("Yuvraj",))
18t3 = Thread(target=wish, args=("Kohli",))
19 
20t1.start()
21t2.start()
22t3.start()
Output
No output captured.

Output

The document does not print the complete output of this program.

It states:

At a time only one Thread is allowed to execute the wish() method and hence we will get regular output.

Conceptually, one possible output order is:

Good Evening:Dhoni
Good Evening:Dhoni
Good Evening:Dhoni
...
10 times

Good Evening:Yuvraj
Good Evening:Yuvraj
Good Evening:Yuvraj
...
10 times

Good Evening:Kohli
Good Evening:Kohli
Good Evening:Kohli
...
10 times

The exact order in which Dhoni, Yuvraj, and Kohli acquire the Lock is not guaranteed.

For example, Yuvraj may acquire the Lock first. The important point is that the outputs of the protected wish() executions do not mix with each other.

Complete Program Explanation

Step 1: Import Required Modules

from threading import *
import time

The threading module provides Thread and Lock functionality.

The time module provides sleep().


Step 2: Create the Lock Object

l = Lock()

A single Lock object is created.

This Lock is shared by all three Threads.

             Lock l
               ▲
          ┌────┼────┐
          │    │    │
          │    │    │
         t1   t2   t3

Step 3: Create the Shared Function

def wish(name):

All three Threads execute the same wish() function.


Step 4: Acquire the Lock

l.acquire()

Before entering the critical section, the Thread must acquire the Lock.

Only one Thread can successfully acquire the Lock at a time.

The remaining Threads wait.


Step 5: Execute the Loop

for i in range(10):

The Thread that owns the Lock executes the loop ten times.


Step 6: Print Good Evening

print("Good Evening:", end='')

The greeting is printed without moving to the next line.


Step 7: Sleep for Two Seconds

time.sleep(2)

The currently executing Thread waits for two seconds.

Even while it is sleeping, it still holds the Lock because release() has not yet been called.

Therefore, the other Threads cannot enter the protected section using this Lock.


Step 8: Print the Name

print(name)

The Thread prints its corresponding name.


Step 9: Complete All Ten Iterations

The Thread continues until all ten iterations are completed.


Step 10: Release the Lock

l.release()

After completing the entire loop, the Thread releases the Lock.

Now one of the waiting Threads can acquire it.


Step 11: Create Three Threads

t1 = Thread(target=wish, args=("Dhoni",))
t2 = Thread(target=wish, args=("Yuvraj",))
t3 = Thread(target=wish, args=("Kohli",))
Thread Target Argument
t1 wish() Dhoni
t2 wish() Yuvraj
t3 wish() Kohli

Step 12: Start All Threads

t1.start()
t2.start()
t3.start()

All three Threads start and request the same Lock.

Only one gets the Lock.

The remaining two wait until the Lock becomes available.

How Three Threads Use One Lock

                 Lock l
                   │
          ┌────────┼────────┐
          │        │        │
          ▼        ▼        ▼
         t1       t2       t3
          │        │        │
          └────────┼────────┘
                   │
                   ▼
             Who Acquires
              Lock First?
                   │
                   ▼
              One Thread
                   │
                   ▼
              wish(name)
                   │
                   ▼
              10 Iterations
                   │
                   ▼
               release()
                   │
                   ▼
             Next Waiting
                Thread

Why Is the Output Regular?

The output becomes regular because:

  • Only one Thread can hold the Lock at a time.
  • The remaining Threads wait until the Lock is released.
  • Only the Lock-owning Thread executes the protected wish() work.
  • The Lock is released only after all ten iterations are completed.

Therefore, we do not get the earlier mixed output such as:

Good Evening:Good Evening:Yuvraj
Dhoni

Instead, each Thread completes its protected execution before another waiting Thread gets the Lock.

Lock is Held During sleep()

An important point in this program is:

l.acquire()

for i in range(10):
    print("Good Evening:", end='')
    time.sleep(2)
    print(name)

l.release()

The Lock is acquired before the loop and released after the loop.

Therefore, even when the current Thread executes:

time.sleep(2)

it does not release the Lock.

Thread-1
   │
   ▼
Acquire Lock
   │
   ▼
Print
   │
   ▼
sleep(2)
   │
   │ Lock Still Held
   │
   ├──── Thread-2 → Waiting
   └──── Thread-3 → Waiting
   │
   ▼
Continue Loop

Execution Flow

Program Starts
      │
      ▼
Create Lock
      │
      ▼
Create t1, t2, t3
      │
      ▼
Start All Threads
      │
      ▼
All Request Same Lock
      │
      ▼
One Thread Acquires Lock
      │
      ├──────────────► Other Thread Waits
      │
      └──────────────► Other Thread Waits
      │
      ▼
Execute wish()
10 Times
      │
      ▼
Release Lock
      │
      ▼
One Waiting Thread
Acquires Lock
      │
      ▼
Execute wish()
10 Times
      │
      ▼
Release Lock
      │
      ▼
Last Waiting Thread
Acquires Lock
      │
      ▼
Execute wish()
10 Times
      │
      ▼
Release Lock
      │
      ▼
All Threads Complete
      │
      ▼
Program Ends

Without Lock vs With Lock

Without Lock With Lock
Multiple Threads may enter shared code simultaneously. Only one Thread can hold the Lock at a time.
Execution overlaps. Protected execution is controlled.
Output can become mixed. Output becomes regular.
Data inconsistency may occur. Data inconsistency can be avoided.
No Thread waits for a Lock. Other Threads wait while the Lock is held.

Lock State

A simple Lock can be understood using two states:

State Meaning
Unlocked The Lock is available to be acquired.
Locked The Lock has already been acquired.
Unlocked
   │
   │ acquire()
   ▼
 Locked
   │
   │ release()
   ▼
Unlocked

Lock Method Comparison

Method Purpose Result
l.acquire() Request the Lock Thread acquires it if available; otherwise it waits
l.release() Release the Lock Lock becomes available

Important Error

Remember this error:

RuntimeError: release unlocked lock

It occurs when:

l = Lock()

l.release()

because the Lock is already unlocked.

The valid order is:

l = Lock()

l.acquire()
l.release()

Summary

  • Lock is a fundamental synchronization mechanism provided by the threading module.
  • A Lock object is created using:
l = Lock()
  • A Lock can be held by only one Thread at a time.
  • If another Thread requires the same Lock, it waits until the current Thread releases it.
  • A Thread acquires the Lock using:
l.acquire()
  • A Thread releases the Lock using:
l.release()
  • Calling release() when the Lock is not acquired raises:
RuntimeError: release unlocked lock
  • Using a Lock ensures that only one Thread executes the protected critical section at a time.
  • This produces regular output in the wish() example.

Important Notes

  1. A Lock object allows only one Thread to hold the Lock at a time.
  2. Other Threads wait until the Lock becomes available.
  3. Use acquire() before entering the critical section.
  4. Use release() after completing the critical section.
  5. The Lock must be in the acquired state before release() is called.
  6. Calling release() on an unlocked Lock results in RuntimeError: release unlocked lock.
  7. In the synchronization example, all three Threads share the same Lock.
  8. The Threads execute the same wish() function.
  9. Thread t1 receives "Dhoni".
  10. Thread t2 receives "Yuvraj".
  11. Thread t3 receives "Kohli".
  12. Only one Thread can execute the Lock-protected wish() work at a time.
  13. The other Threads wait until the Lock is released.
  14. The Lock is acquired before the loop and released after all ten iterations.
  15. Therefore, the Thread continues holding the Lock even during time.sleep(2).
  16. The synchronized program produces regular output instead of the mixed output seen without synchronization.
📝 Key Takeaways
  • A Lock allows only one thread in the critical section at a time
  • Other threads block while the lock is held
  • Calling release() on an unlocked lock raises RuntimeError
  • The synchronized wish() program produces regular output

🧠 Test Your Knowledge

15 Questions
Progress: 0 / 15