Back

Writing efficient and fair multi-threaded programs !!

Jul 29 2023
10min
🕐 Current time : 29 Mar 2025, 05:04 AM
The full Astro logo.

The ever-increasing demand for computing power and the prevalence of multi-core processors have driven the adoption of multi-threaded programming. Efficient and fair multi-threaded programs are essential for maximizing system resources and delivering responsive applications. However, writing such programs can be challenging due to the complexities of concurrent execution. This edition delves into the principles and best practices that developers should follow to achieve efficiency and fairness in multi-threaded programming.Writing efficient and fair multi-threaded programs can be challenging, but following some best practices and design principles can help you achieve your goals. Let’s look into some examples that can help us to write efficient and fair multi-threaded programs.

  1. Understand Your Problem Domain:

Before writing any multi-threaded code, you should have a clear understanding of the problem you’re trying to solve and determine if it can benefit from parallelism. For example, if you need to download multiple files from the internet simultaneously, multi-threading could be useful.

  • Use High-Level Abstractions.

  • Python’s threading module provides high-level abstractions through the concurrent.futures module. The ThreadPoolExecutor and ProcessPoolExecutor classes allow you to work with a pool of threads or processes, respectively. let’s code an example using ThreadPoolExecutor.

import concurrent.futures


def task_function(task_id):
    print(f"Task {task_id} is running.")
    return f"Result from task {task_id}"


def main():
    with concurrent.futures.ThreadPoolExecutor() as executor:
        tasks = [executor.submit(task_function, i) for i in range(5)]


        for future in concurrent.futures.as_completed(tasks):
            result = future.result()
            print(result)


if __name__ == "__main__":
    main() 
  1. Minimize Shared Data:

As much as possible, try to avoid sharing data between threads. If data sharing is necessary, consider using thread-safe data structures like queue.Queue to minimize potential issues. Let’s code it below.

import threading
import queue


def producer(queue):
    for i in range(5):
        queue.put(i)


def consumer(queue):
    while True:
        item = queue.get()
        if item is None:
            break
        print(f"Consumed item: {item}")
        queue.task_done()


def main():
    shared_queue = queue.Queue()
    producer_thread = threading.Thread(target=producer, args=(shared_queue,))
    consumer_thread = threading.Thread(target=consumer, args=(shared_queue,))


    producer_thread.start()
    consumer_thread.start()


    producer_thread.join()
    shared_queue.put(None)
    consumer_thread.join()


if __name__ == "__main__":
    main()
  1. Synchronize Access to Shared Data:

When sharing data between threads, use synchronization mechanisms like locks to ensure that only one thread accesses the shared data at a time.

import threading


shared_data = 0
lock = threading.Lock()


def increment_shared_data():
    global shared_data
    for _ in range(100000):
        with lock:
            shared_data += 1


def main():
    threads = [threading.Thread(target=increment_shared_data) for _ in range(5)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()


    print(f"Final shared data value: {shared_data}")


if __name__ == "__main__":
    main() 
  1. Avoid Unnecessary Locking: Avoid excessive locking, as it can lead to contention and decrease performance. One way to do this is by using lock-free data structures, like queue.Queue, which is already designed for concurrent access.

  2. Balance Workload: Balancing workload is essential to maximize the benefits of multi-threading. If you have a large number of tasks to perform, divide the work evenly among threads. You can use a thread pool to achieve this, as shown in the previous examples using ThreadPoolExecutor.

  3. Use Thread Pools: Using thread pools helps reduce the overhead of creating and destroying threads. Python’s concurrent.futures.ThreadPoolExecutor provides a thread pool that you can use to submit tasks.

import concurrent.futures


def task_function(task_id):
    return f"Result from task {task_id}"


def main():
    with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
        tasks = [executor.submit(task_function, i) for i in range(10)]


        for future in concurrent.futures.as_completed(tasks):
            result = future.result()
            print(result)


if __name__ == "__main__":
    main() 
  1. Consider Asynchronous I/O: For I/O-bound tasks, consider using asynchronous I/O to avoid blocking threads while waiting for I/O operations to complete. The asyncio module in Python provides an easy way to work with asynchronous I/O. Test Thoroughly: Testing is critical for multi-threaded programs. Ensure that your code works correctly and consistently under various scenarios. Use tools like unittest to write test cases and verify the correctness of your code.

  2. Monitor and Profile: Use monitoring and profiling tools to identify bottlenecks and performance issues in your multi-threaded program. Python provides built-in modules like cProfile and external tools like snakeviz for profiling.

  3. Consider Task-Based Design: Organize your program into small, manageable tasks that can be executed independently. Task-based design can make it easier to reason about thread interactions and improve the overall efficiency.

  4. Be Mindful of Fairness: If your multi-threaded program needs to provide fairness guarantees, design your synchronization and scheduling mechanisms accordingly. Python’s threading.Condition can be helpful for scenarios where threads need to wait for specific conditions to be met.

  5. Avoid Busy Waiting: Busy-waiting loops waste CPU cycles and can lead to high CPU usage. Instead, use blocking mechanisms like condition variables or semaphores to allow threads to wait efficiently.

Remember that multi-threaded programming can be complex, and ensuring efficiency and fairness requires careful consideration and testing. The sample code provided above serves as a starting point, but real-world applications may require additional considerations and optimizations. 💡

Read more in this Series:

Find me on

GitHub LinkedIn LinkedIn X Twitter
© 2022 to 2025 : Amit Prakash