杰瑞科技汇

Python锁acquire会阻塞吗?

Of course! In Python, a lock.acquire() is a fundamental method used for thread synchronization. It's part of the threading module and is essential for preventing race conditions when multiple threads try to access and modify a shared resource (like a variable or a file) simultaneously.

Python锁acquire会阻塞吗?-图1
(图片来源网络,侵删)

Let's break it down with clear explanations and examples.

The Core Problem: Race Conditions

Imagine you have a shared variable, let's say a counter, and two threads that both want to increment it.

  1. Initial State: counter = 0
  2. Thread 1: Reads the value of counter (it's 0).
  3. Thread 2: Reads the value of counter (it's also 0).
  4. Thread 1: Adds 1 to its local copy. The new value is 1.
  5. Thread 2: Adds 1 to its local copy. The new value is also 1.
  6. Thread 1: Writes its new value (1) back to counter.
  7. Thread 2: Writes its new value (1) back to counter.

Final State: counter = 1

Expected Result: counter = 2

Python锁acquire会阻塞吗?-图2
(图片来源网络,侵删)

The final result is wrong because both threads read the same initial value before either could write back. A lock solves this by ensuring that only one thread can execute the critical section (the code that modifies the shared resource) at a time.


How lock.acquire() Works

A lock has two states: locked and unlocked.

  • lock.acquire():

    • If the lock is unlocked, the calling thread acquires it, and the lock becomes locked. The thread then continues executing the code inside the with block or after the acquire() call.
    • If the lock is already locked by another thread, the calling thread will block (wait) until the lock is released by the other thread.
  • lock.release():

    Python锁acquire会阻塞吗?-图3
    (图片来源网络,侵删)
    • This method releases the lock.
    • If the lock was locked, it becomes unlocked.
    • If one or more threads are waiting to acquire the lock, one of them (usually the one that waited the longest) will be woken up and will acquire the lock.

The Best Practice: The with Statement

Manually calling acquire() and release() is error-prone. What if an exception occurs between them? The lock would never be released, causing a deadlock.

The modern, safe, and recommended way to use locks is with the with statement. It automatically handles the acquire() at the beginning of the block and release() at the end, even if an exception occurs.

# The safe way:
with my_lock:
    # Critical section code here
    # This code is protected by the lock
    pass
# This is equivalent to (but safer than):
# my_lock.acquire()
# try:
#     # Critical section code here
#     pass
# finally:
#     my_lock.release()

Complete Example: The Broken Counter (Without a Lock)

This example demonstrates the race condition. We'll use a global counter and a helper function that runs in a thread to increment it many times.

import threading
# A shared resource that multiple threads will access
shared_counter = 0
# Number of times each thread will increment the counter
NUM_INCREMENTS = 1_000_000
def increment_counter():
    """A function that increments the shared counter."""
    global shared_counter
    for _ in range(NUM_INCREMENTS):
        shared_counter += 1
# Create two threads that will both run the increment function
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)
# Start the threads
thread1.start()
thread2.start()
# Wait for both threads to complete their execution
thread1.join()
thread2.join()
print(f"Final counter value: {shared_counter}")
print(f"Expected counter value: {2 * NUM_INCREMENTS}")

Typical Output (will vary):

Final counter value: 1358765
Expected counter value: 2000000

As you can see, the final value is much lower than expected because the threads are overwriting each other's changes.


Corrected Example: The Protected Counter (With a Lock)

Now, let's fix the problem by using a threading.Lock.

import threading
# A shared resource
shared_counter = 0
# Number of increments per thread
NUM_INCREMENTS = 1_000_000
# Create a lock object
lock = threading.Lock()
def increment_counter_safe():
    """A function that safely increments the shared counter using a lock."""
    global shared_counter
    for _ in range(NUM_INCREMENTS):
        # Use the 'with' statement to safely acquire and release the lock
        with lock:
            # This is the "critical section". Only one thread can be here at a time.
            shared_counter += 1
# Create two threads
thread1 = threading.Thread(target=increment_counter_safe)
thread2 = threading.Thread(target=increment_counter_safe)
# Start the threads
thread1.start()
thread2.start()
# Wait for both threads to finish
thread1.join()
thread2.join()
print(f"Final counter value: {shared_counter}")
print(f"Expected counter value: {2 * NUM_INCREMENTS}")

Output:

Final counter value: 2000000
Expected counter value: 2000000

This time, the result is correct. The with lock: statement ensures that when one thread is executing shared_counter += 1, the other thread must wait until the first thread is done and has released the lock.


Other Useful Lock Methods

  1. lock.acquire(blocking=True, timeout=-1)

    • This is the full signature of the acquire method.
    • blocking: If True (the default), the call will block until the lock is acquired. If False, the call will return False immediately if the lock is already acquired, and True if it was acquired successfully.
    • timeout: If blocking is True, you can specify a timeout in seconds. The call will block for at most that many seconds. If the lock isn't acquired by then, it returns False.

    Example of non-blocking acquire:

    if lock.acquire(blocking=False):
        print("Lock acquired successfully!")
        # do work
        lock.release()
    else:
        print("Could not acquire the lock, it's already taken.")
  2. lock.locked()

    • Returns True if the lock is currently acquired by some thread, and False otherwise. This is useful for checking the state of the lock without waiting.

    Example:

    if lock.locked():
        print("The lock is currently in use.")
    else:
        print("The lock is free.")

Summary: When to Use a Lock

Use a threading.Lock when you have:

  • Shared Resources: Variables, lists, dictionaries, or other data structures that are accessed by multiple threads.
  • Non-Atomic Operations: Operations that consist of multiple steps (like "read, modify, write") where the intermediate state could be seen by another thread, leading to incorrect results.

By using locks correctly, you ensure data integrity and prevent race conditions in your multi-threaded Python applications.

分享:
扫描分享到社交APP
上一篇
下一篇