Hone logo
Hone
Problems

Implementing Thread Safety with Locks in Python

In concurrent programming, multiple threads might attempt to access and modify shared resources simultaneously. This can lead to race conditions, where the outcome of the program depends on the unpredictable order of thread execution, often resulting in corrupted data or unexpected behavior. This challenge focuses on implementing a fundamental mechanism to prevent such issues: locks. You will create a system that simulates multiple threads accessing a shared counter, and you'll use locks to ensure that these operations are thread-safe.

Problem Description

Your task is to implement a class that manages a shared counter and allows multiple threads to increment it concurrently. You need to ensure that the counter is incremented accurately, even when accessed by many threads simultaneously. This will be achieved by implementing a thread-safe mechanism using Python's built-in threading locks.

Requirements:

  1. ThreadSafeCounter Class: Create a class named ThreadSafeCounter.
  2. Initialization: The class should be initialized with an optional starting value for the counter (defaulting to 0).
  3. increment() Method: This method should atomically increment the counter by 1. "Atomically" means that the entire operation (reading the current value, adding 1, and writing the new value back) must complete without interruption from other threads.
  4. get_value() Method: This method should return the current value of the counter. This operation should also be protected by a lock to ensure a consistent read.
  5. Lock Implementation: Use threading.Lock from Python's threading module to protect the shared counter resource.

Expected Behavior:

When multiple threads call the increment() method on the same ThreadSafeCounter instance, the final value of the counter should precisely reflect the total number of increments performed across all threads, without any lost updates due to race conditions.

Edge Cases to Consider:

  • What happens if no threads are accessing the counter?
  • What happens if only one thread is accessing the counter?
  • How does the lock behave if increment() is called multiple times within a single thread before another thread gets a chance to run?

Examples

Example 1:

import threading
import time

# Assume ThreadSafeCounter is implemented as described

counter = ThreadSafeCounter(0)
num_threads = 10
increments_per_thread = 1000

def worker():
    for _ in range(increments_per_thread):
        counter.increment()

threads = []
for _ in range(num_threads):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

final_value = counter.get_value()
print(f"Final counter value: {final_value}")

Output:

Final counter value: 10000

Explanation:

10 threads are created, and each thread increments the counter 1000 times. Without proper locking, some increments might be lost. With a correctly implemented lock, each of the 10 * 1000 = 10000 increments should be registered, resulting in a final value of 10000.

Example 2:

import threading
import time

# Assume ThreadSafeCounter is implemented as described

counter = ThreadSafeCounter(5) # Starting with 5
num_threads = 5
increments_per_thread = 500

def worker():
    for _ in range(increments_per_thread):
        counter.increment()

threads = []
for _ in range(num_threads):
    thread = threading.Thread(target=worker)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

final_value = counter.get_value()
print(f"Final counter value: {final_value}")

Output:

Final counter value: 2505

Explanation:

The counter starts at 5. 5 threads each increment it 500 times. The total increments are 5 * 500 = 2500. The final value is the initial value plus total increments: 5 + 2500 = 2505.

Constraints

  • The ThreadSafeCounter class must be implemented in Python.
  • The solution must use threading.Lock for synchronization.
  • The increment() and get_value() methods should be thread-safe.
  • Avoid using higher-level concurrency primitives like multiprocessing.Value or semaphores for this specific problem, focusing purely on threading.Lock.
  • The number of threads and increments per thread can be substantial, so efficiency in lock acquisition/release is implicitly important.

Notes

  • Remember that a lock needs to be acquired before accessing the shared resource and released after the operation is complete.
  • Consider using a with statement for acquiring and releasing the lock, as it handles the release automatically even if errors occur.
  • The threading module is your primary tool here.
  • Think about what operations are considered "critical sections" – the parts of your code that must be executed by only one thread at a time.
Loading editor...
python