2 threads accessing at the same time to a synchronized function

3 min read 07-10-2024
2 threads accessing at the same time to a synchronized function


The Dance of Threads: Understanding Synchronization in Multithreaded Environments

In the world of software development, multithreading offers the promise of improved performance by dividing tasks and executing them concurrently. However, this power comes with a crucial caveat: managing shared resources becomes a complex balancing act. Imagine two dancers, each wanting to use the same prop at the same time – chaos ensues! This is precisely the problem we face when multiple threads attempt to access and modify the same data simultaneously.

The Scenario: Two Threads, One Shared Function

Let's illustrate this with a simple example. Consider a function incrementCounter that increments a shared counter variable.

counter = 0

def incrementCounter():
  global counter
  counter += 1

Now, let's create two threads, thread1 and thread2, each calling incrementCounter 10 times.

import threading

def thread_function(thread_name):
  for _ in range(10):
    incrementCounter()
  print(f"{thread_name}: Counter is {counter}")

thread1 = threading.Thread(target=thread_function, args=("Thread 1",))
thread2 = threading.Thread(target=thread_function, args=("Thread 2",))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

You might expect the final counter value to be 20 (10 increments from each thread). However, the reality is often less predictable. The actual outcome varies depending on the precise timing of the threads.

The Problem: The issue lies in the fact that incrementCounter is not synchronized. When multiple threads execute this function concurrently, the following race condition can occur:

  1. Thread 1 reads the value of counter (let's say it's 5).
  2. Thread 2 also reads the value of counter (also 5).
  3. Thread 1 increments the value of counter to 6.
  4. Thread 2 increments the value of counter to 6 (instead of 7).

This leads to unexpected results where the counter value doesn't reflect the combined efforts of the threads.

Enter Synchronization: The Choreographer of Threads

To prevent these race conditions and ensure data integrity, we need to introduce synchronization. This acts like a choreographer, ensuring threads take turns accessing and modifying shared resources.

The Solution: synchronized Keyword

Python offers the synchronized keyword to achieve this. We can apply it to the incrementCounter function:

import threading

counter = 0

lock = threading.Lock()

def incrementCounter():
  global counter
  with lock:
    counter += 1

def thread_function(thread_name):
  for _ in range(10):
    incrementCounter()
  print(f"{thread_name}: Counter is {counter}")

thread1 = threading.Thread(target=thread_function, args=("Thread 1",))
thread2 = threading.Thread(target=thread_function, args=("Thread 2",))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

In this modified code, we've introduced a lock object. The with lock: statement ensures that only one thread can execute the code within the block at a time. This creates a "critical section" where the counter variable is protected from race conditions. Now, when one thread holds the lock, other threads are blocked from entering the incrementCounter function until the lock is released. This ensures that the counter is incremented correctly, and we consistently get the expected result of 20.

Key Takeaways:

  • Multithreading can improve performance but requires careful management of shared resources.
  • Race conditions occur when multiple threads attempt to access and modify shared data concurrently.
  • Synchronization mechanisms like locks are crucial for preventing race conditions and ensuring data integrity.
  • The synchronized keyword (or its equivalent in different languages) provides a simple yet effective way to synchronize access to shared resources.

Further Exploration:

  • Other Synchronization Mechanisms: Explore other synchronization techniques like semaphores, condition variables, and mutexes.
  • Deadlocks: Learn about potential issues like deadlocks that can arise in multithreaded scenarios due to improper lock management.
  • Thread Pools: Investigate thread pools for efficiently managing a pool of threads to handle tasks.

By understanding and effectively utilizing synchronization techniques, you can harness the power of multithreading while maintaining the integrity of your applications.