【Python】Thread Exclusive Control: Preventing Contention with threading.Lock

目次

Overview

In multithreading, when multiple threads try to modify the same variable or resource (files, databases, global variables, etc.) at the same time, data can lose consistency. This is called a “Race Condition.” To prevent this, we use threading.Lock. This article explains how to implement “Exclusive Control,” which makes other threads wait while one thread is processing.

Specifications (Input/Output)

  • Input: A function that operates on shared resources (variables, etc.).
  • Output: Correctly calculated results without contention.
  • Features:
    • Protection of critical sections (parts that should not run simultaneously).
    • Synchronization between threads.

Syntax and Meaning

SyntaxMeaning / Role
lock = threading.Lock()Creates a new lock object. Initial state is “unlocked.”
with lock:Uses a context manager to acquire the lock. It is automatically released (Release) when exiting the block. This is the recommended style.
lock.acquire()Manually acquires the lock. If another thread is currently locking, it waits until released.
lock.release()Manually releases the lock. The thread that acquired it must call this.

Basic Usage

The basic pattern is to create a lock object and wrap the process you want to protect with a with statement.

import threading

# 1. Create a lock object
lock = threading.Lock()
shared_data = 0

def safe_update():
    global shared_data
    
    # 2. Start exclusive control
    # Only one thread can execute inside this block at any time
    with lock:
        current = shared_data
        current += 1
        shared_data = current

# Create and start threads after this...

Full Code

This code simulates the difference between “unsafe (no lock)” and “safe (with lock)” states. Using a bank account balance update as an example, it demonstrates how calculations go wrong without exclusive control and how Lock fixes it.

import threading
import time

# Shared resource (assuming a bank account balance)
account_balance = 0

# Create a lock object for exclusive control
balance_lock = threading.Lock()

def update_balance_unsafe(amount, loops):
    """
    [UNSAFE] Function to update balance without a lock.
    Contention occurs easily because of the delay between reading and writing.
    """
    global account_balance
    for _ in range(loops):
        # Read the current value
        current = account_balance
        
        # Simulating time lag like IO wait
        time.sleep(0.001)
        
        # Calculate and write back (another thread might have overwritten the value here)
        account_balance = current + amount

def update_balance_safe(amount, loops):
    """
    [SAFE] Function to update balance using a lock.
    """
    global account_balance
    for _ in range(loops):
        # --- Start Critical Section ---
        with balance_lock:
            current = account_balance
            time.sleep(0.001)
            account_balance = current + amount
        # --- End Critical Section ---

def run_simulation(safe_mode=True):
    global account_balance
    account_balance = 0 # Reset
    
    threads = []
    loops = 50
    thread_count = 5
    
    print(f"--- Starting Simulation (SafeMode: {safe_mode}) ---")
    
    target_func = update_balance_safe if safe_mode else update_balance_unsafe
    
    # Perform updates simultaneously in multiple threads
    for _ in range(thread_count):
        t = threading.Thread(target=target_func, args=(1, loops))
        threads.append(t)
        t.start()
        
    for t in threads:
        t.join()
        
    # Expected value: Number of threads * Number of loops
    expected = thread_count * loops
    print(f"Final Balance: {account_balance} (Expected: {expected})")
    
    if account_balance == expected:
        print(">> Result: Success")
    else:
        print(">> Result: Data inconsistency occurred!")
    print("-" * 30)

if __name__ == "__main__":
    # 1. Run without a lock (likely to fail)
    run_simulation(safe_mode=False)
    
    # 2. Run with a lock (always succeeds)
    run_simulation(safe_mode=True)

Customization Points

Benefits of using the with statement

You can manually write lock.acquire() and lock.release(), but it is not recommended. By using with lock:, the lock is automatically released even if an error (exception) occurs within the block. This prevents “Deadlocks,” where a thread stops due to an error while holding a lock, causing other threads to wait forever.

Adjusting Granularity

A golden rule is to keep the scope of the lock (the content inside with) to a minimum. If you include unnecessary calculations or heavy communication inside the lock, the benefits of parallel processing are lost, and the program slows down as it becomes essentially single-threaded.

Important Notes

Deadlock

If there are multiple locks (Lock A, Lock B), a deadlock can occur if Thread 1 holds A and waits for B, while Thread 2 holds B and waits for A. You need a design that unifies the order of lock acquisition.

Recursive Locking

A normal Lock will freeze even its own thread if acquire is called twice. If the same thread needs to acquire a lock multiple times (e.g., in recursive calls), use threading.RLock.

Performance

Acquiring and releasing locks has overhead. If you lock too frequently, it may actually decrease the processing speed.

Advanced Usage

Example using RLock (Reentrant Lock). The same thread can acquire the lock in a nested way.

import threading

# Use RLock
rlock = threading.RLock()

def recursive_worker(count):
    with rlock:
        print(f"Lock acquired: Depth {count}")
        if count > 0:
            # A normal Lock would freeze here, but RLock allows it
            recursive_worker(count - 1)
            
if __name__ == "__main__":
    recursive_worker(3)

Summary

Always use threading.Lock for exclusive control when handling shared data in multiple threads. The basic approach is using with lock:. By making the “read, calculate, and write” flow an atomic (indivisible) operation, you can protect data integrity.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次