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.
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 : -525674The 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: 03. 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 again5. 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
