Fundamentals 26 min read

Understanding Python Threading Locks: Lock, RLock, Condition, Event, and Semaphore

This article provides a comprehensive tutorial on Python's threading module, explaining thread safety concepts and demonstrating the use of various lock primitives—including Lock, RLock, Condition, Event, and Semaphore—through detailed code examples and practical exercises.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Understanding Python Threading Locks: Lock, RLock, Condition, Event, and Semaphore

Preface

This article continues the discussion of the threading module, focusing mainly on theory.

Although everyday developers rarely need this content, it is essential knowledge for framework authors and a frequent interview topic.

Thread Safety

Thread safety is a concept in multithreaded or multiprocess programming where shared data is accessed correctly by synchronizing thread execution, preventing data corruption.

Consider a room with 10 candies (resource) and three people (1 main thread, 2 child threads). If one thread eats 3 candies and is paused, it thinks 7 remain, while the other thread may also eat 3, leading to an inconsistent view of the remaining candies.

The following example demonstrates this problem with a shared variable num initialized to 0 and two threads performing one million increments and decrements respectively.

import threading

num = 0

def add():
    global num
    for i in range(10_000_000):
        num += 1

def sub():
    global num
    for i in range(10_000_000):
        num -= 1

if __name__ == "__main__":
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)
    subThread01.start()
    subThread02.start()
    subThread01.join()
    subThread02.join()
    print("num result : %s" % num)
    # Sample results:
    # num result : 669214
    # num result : -1849179
    # num result : -525674

The unexpected results show that without proper synchronization the final value is not zero. The solution is to use locks to control thread switching.

Note that built‑in containers like list, tuple, and dict are thread‑safe for individual operations, so they do not require additional locking.

Purpose of Locks

Locks allow developers to control when threads can switch, making the execution order deterministic. By acquiring a lock before accessing shared data and releasing it afterward, thread safety is ensured.

The threading module provides five common lock types:

Synchronous lock: Lock (only one thread can hold it at a time)

Recursive lock: RLock (allows the same thread to acquire multiple times)

Condition lock: Condition (can release any number of waiting threads)

Event lock: Event (releases all waiting threads at once)

Semaphore lock: Semaphore (releases a specified number of threads)

1. Lock() – Synchronous Lock

Basic Introduction

The lock is also called a mutex; only one thread can execute the protected code block at a time.

Mutual exclusion means a resource can be accessed by only one thread at a time.

Synchronization adds ordering on top of mutual exclusion.

Relevant threading methods:

Method

Description

threading.Lock()

Return a synchronous lock object

lockObject.acquire(blocking=True, timeout=1)

Acquire the lock (default timeout 1 s)

lockObject.release()

Release the lock

lockObject.locked()

Return True if the lock is held

Usage

A synchronous lock allows only one thread to run the critical section; other threads wait until the lock is released.

Example that makes the previous counter code thread‑safe:

import threading

num = 0

def add():
    lock.acquire()
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()

def sub():
    lock.acquire()
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()

if __name__ == "__main__":
    lock = threading.Lock()
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)
    subThread01.start()
    subThread02.start()
    subThread01.join()
    subThread02.join()
    print("num result : %s" % num)
    # Expected result: 0 (multiple runs)

Note that for CPU‑bound tasks this serializes execution and may be slower than a single‑threaded approach; the example is for illustration only.

2. RLock() – Recursive Lock

Basic Introduction

A recursive lock permits the same thread to acquire the lock multiple times, provided each acquire is matched with a release.

Relevant methods:

Method

Description

threading.RLock()

Return a recursive lock object

lockObject.acquire(blocking=True, timeout=1)

Acquire the lock

lockObject.release()

Release the lock

lockObject.locked()

Check lock state

Usage

With a recursive lock the following code does not deadlock, whereas using a plain Lock would:

import threading

num = 0

def add():
    lock.acquire()
    lock.acquire()
    global num
    for i in range(10_000_000):
        num += 1
    lock.release()
    lock.release()

def sub():
    lock.acquire()
    lock.acquire()
    global num
    for i in range(10_000_000):
        num -= 1
    lock.release()
    lock.release()

if __name__ == "__main__":
    lock = threading.RLock()
    subThread01 = threading.Thread(target=add)
    subThread02 = threading.Thread(target=sub)
    subThread01.start()
    subThread02.start()
    subThread01.join()
    subThread02.join()
    print("num result : %s" % num)
    # Expected result: 0

3. Condition() – Condition Lock

Basic Introduction

A condition lock builds on a recursive lock and adds the ability to pause threads using wait() and resume them with notify() or notify_all().

Key methods:

Method

Description

threading.Condition()

Return a condition lock object

lockObject.acquire(blocking=True, timeout=1)

Acquire the underlying lock

lockObject.release()

Release the underlying lock

lockObject.wait(timeout=None)

Put the current thread into waiting state

lockObject.wait_for(predicate, timeout=None)

Wait until predicate returns True lockObject.notify(n=1)

Wake up n waiting threads

lockObject.notify_all()

Wake up all waiting threads

Usage Example

The program launches ten child threads, puts them all into waiting state, and then the main thread notifies a chosen number of threads to continue:

import threading

currentRunThreadNumber = 0
maxSubThreadNumber = 10

def task():
    global currentRunThreadNumber
    thName = threading.currentThread().name
    condLock.acquire()
    print("start and wait run thread : %s" % thName)
    condLock.wait()
    currentRunThreadNumber += 1
    print("carry on run thread : %s" % thName)
    condLock.release()

if __name__ == "__main__":
    condLock = threading.Condition()
    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()
    while currentRunThreadNumber < maxSubThreadNumber:
        notifyNumber = int(input("Please enter the number of threads that need to be notified to run:"))
        condLock.acquire()
        condLock.notify(notifyNumber)
        condLock.release()
    print("main thread run end")

4. Event() – Event Lock

Basic Introduction

An event lock is based on a condition lock but can only release all waiting threads at once, similar to a traffic light turning green.

Key methods:

Method

Description

threading.Event()

Return an event lock object

lockObject.clear()

Set the event to red (threads wait)

lockObject.is_set()

Check if the event is green

lockObject.set()

Set the event to green (release all)

lockObject.wait(timeout=None)

Block until the event becomes green

Usage

Example simulating a red‑green light for three threads:

import threading

maxSubThreadNumber = 3

def task():
    thName = threading.currentThread().name
    print("start and wait run thread : %s" % thName)
    eventLock.wait()
    print("green light, %s carry on run" % thName)
    print("red light, %s stop run" % thName)
    eventLock.wait()
    print("green light, %s carry on run" % thName)
    print("sub thread %s run end" % thName)

if __name__ == "__main__":
    eventLock = threading.Event()
    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()
    eventLock.set()   # green
    eventLock.clear() # red
    eventLock.set()   # green again

5. Semaphore() – Semaphore Lock

Basic Introduction

A semaphore limits the number of threads that can hold the lock simultaneously, acting like a narrow road that only allows a fixed batch of cars.

Key methods:

Method

Description

threading.Semaphore()

Return a semaphore lock object

lockObject.acquire(blocking=True, timeout=1)

Acquire a slot

lockObject.release()

Release a slot

Usage Example

Only two threads may run the critical section at a time:

import threading, time

maxSubThreadNumber = 6

def task():
    thName = threading.currentThread().name
    semaLock.acquire()
    print("run sub thread %s" % thName)
    time.sleep(3)
    semaLock.release()

if __name__ == "__main__":
    semaLock = threading.Semaphore(2)  # allow 2 concurrent threads
    for i in range(maxSubThreadNumber):
        subThreadIns = threading.Thread(target=task)
        subThreadIns.start()

Lock Relationship Overview

All five lock types are built on the basic synchronous lock. RLock adds a counter, Condition combines a low‑level lock with a high‑level recursive lock, and both Event and Semaphore are implemented using a Condition internally.

Basic Practice Problems

Condition Lock Application

Goal: Fill a list with numbers 1‑100 in order using two threads—one adds even numbers, the other adds odd numbers.

import threading

lst = []

def even():
    """Add even numbers"""
    with condLock:
        for i in range(2, 101, 2):
            if len(lst) % 2 != 0:
                lst.append(i)
                condLock.notify()
                condLock.wait()
            else:
                condLock.wait()
                lst.append(i)
                condLock.notify()
        condLock.notify()

def odd():
    """Add odd numbers"""
    with condLock:
        for i in range(1, 101, 2):
            if len(lst) % 2 == 0:
                lst.append(i)
                condLock.notify()
                condLock.wait()
            else:
                condLock.wait()
                lst.append(i)
                condLock.notify()
        condLock.notify()

if __name__ == "__main__":
    condLock = threading.Condition()
    addEvenTask = threading.Thread(target=even)
    addOddTask = threading.Thread(target=odd)
    addEvenTask.start()
    addOddTask.start()
    addEvenTask.join()
    addOddTask.join()
    print(lst)

Event Lock Application

Two threads represent Li Bai and Du Fu and should speak alternately:

import threading

def libai():
    event.wait()
    print("李白:老杜啊,不喝了我喝不下了!")
    event.set()
    event.clear()
    event.wait()
    print("李白:呼呼呼...睡着了..")

def dufu():
    print("杜甫:老李啊,来喝酒!")
    event.set()
    event.clear()
    event.wait()
    print("杜甫:老李啊,再来一壶?")
    print("杜甫:...老李?")
    event.set()

if __name__ == "__main__":
    event = threading.Event()
    t1 = threading.Thread(target=libai)
    t2 = threading.Thread(target=dufu)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

These examples illustrate how different lock primitives can be used to coordinate thread execution, prevent race conditions, and implement complex synchronization patterns.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

SynchronizationmultithreadingLock
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.