Learning Paths/Interview Mastery/Concurrency & Parallelism

Concurrency & Parallelism

Master the concepts of concurrent and parallel programming to solve complex problems efficiently. This module covers threading models, synchronization mechanisms, and common concurrency patterns used in modern software development.

Introduction to Concurrency & Parallelism

Concurrency and parallelism are related but distinct concepts in computing that deal with executing multiple tasks:

Concurrency

Concurrency is about dealing with multiple things at once. It's the composition of independently executing processes. Concurrent programming enables tasks to overlap in execution, but they may not be executed simultaneously.

Parallelism

Parallelism is about doing multiple things at once. It's the simultaneous execution of computations, possibly related, on multiple processor cores or distributed systems.

Why Study Concurrency & Parallelism?

  • Performance: Utilize multi-core processors and distributed systems effectively
  • Responsiveness: Keep applications responsive while performing intensive operations
  • Resource Utilization: Maximize the use of available computing resources
  • Scalability: Build systems that can scale with increasing workloads
  • Interview Preparation: Common topic in system design and coding interviews

Challenges in Concurrent Programming

  • Race Conditions: When the behavior depends on the relative timing of events
  • Deadlocks: When two or more processes are unable to proceed because each is waiting for the other
  • Livelocks: When processes are actively performing operations but not making progress
  • Starvation: When a process is perpetually denied necessary resources
  • Complexity: Concurrent programs are harder to design, implement, and debug

Interactive Concurrency Visualization

Experiment with common concurrency scenarios to better understand synchronization mechanisms and potential issues like deadlocks and race conditions.

Concurrency Scenarios

The Producer-Consumer problem involves multiple producers creating items and adding them to a shared buffer, and multiple consumers removing items from the buffer.

Speed:50%
Step: 0 / -1

Explanation:

Key Concepts:

  • Bounded Buffer: Shared resource with limited capacity
  • Synchronization: Prevents buffer overflow and underflow
  • Mutual Exclusion: Only one thread can modify the buffer at a time

Threading Models

Threading models define how concurrent execution is structured and managed within a system. Different programming languages and environments implement various threading models.

1. OS Threads

Operating system threads are managed by the OS scheduler and can execute in true parallel on multi-core systems.

Characteristics:

  • Heavy-weight: Each thread requires significant system resources
  • Preemptive Scheduling: OS can interrupt threads to allow others to run
  • True Parallelism: Can execute simultaneously on multiple cores
  • Shared Memory: Threads within the same process share memory space
// Java example of creating OS threads
public class ThreadExample {
    public static void main(String[] args) {
        // Create and start two threads
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread 1: " + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Thread 2: " + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        thread1.start();
        thread2.start();
        
        // Wait for both threads to complete
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("All threads completed");
    }
}

2. Green Threads / User-level Threads

Green threads are managed by a runtime library or virtual machine rather than the operating system.

Characteristics:

  • Light-weight: Require fewer resources than OS threads
  • Cooperative Scheduling: Threads voluntarily yield control
  • No True Parallelism: Multiple green threads may execute on a single OS thread
  • Platform Independence: Behavior is consistent across different operating systems
// Go example using goroutines (a form of green threads)
package main

import (
    "fmt"
    "time"
)

func main() {
    // Create and start two goroutines
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("Goroutine 1:", i)
            time.Sleep(100 * time.Millisecond)
        }
    }()
    
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("Goroutine 2:", i)
            time.Sleep(100 * time.Millisecond)
        }
    }()
    
    // Wait to see the output
    time.Sleep(1 * time.Second)
    fmt.Println("Main function completed")
}

3. Thread Pools

Thread pools manage a collection of worker threads that are reused to execute tasks, avoiding the overhead of thread creation.

Advantages:

  • Resource Management: Limits the number of active threads
  • Performance: Reduces thread creation/destruction overhead
  • Scalability: Can adjust pool size based on system load
  • Task Queuing: Manages tasks when all threads are busy
// Java example using a thread pool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // Create a fixed thread pool with 3 threads
        ExecutorService executor = Executors.newFixedThreadPool(3);
        
        // Submit 5 tasks to the thread pool
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " executed by " + 
                                   Thread.currentThread().getName());
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            });
        }
        
        // Shutdown the executor
        executor.shutdown();
    }
}

4. Asynchronous Programming Models

Asynchronous programming uses non-blocking operations and callbacks or promises to handle concurrent tasks without using multiple threads.

Common Implementations:

  • Event Loop: Single-threaded model that processes events asynchronously (Node.js)
  • Promises/Futures: Represent values that may be available in the future
  • Async/Await: Syntactic sugar for working with promises
  • Reactive Programming: Data flow programming paradigm based on event streams
// JavaScript example using async/await
async function fetchUserData(userId) {
    try {
        // Simulating API calls
        console.log("Fetching user details...");
        const userDetails = await fetchUserDetails(userId);
        
        console.log("Fetching user posts...");
        const userPosts = await fetchUserPosts(userId);
        
        return {
            user: userDetails,
            posts: userPosts
        };
    } catch (error) {
        console.error("Error fetching user data:", error);
        throw error;
    }
}

// Simulated API functions
function fetchUserDetails(userId) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve({ id: userId, name: "John Doe", email: "john@example.com" });
        }, 1000);
    });
}

function fetchUserPosts(userId) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve([
                { id: 1, title: "First Post" },
                { id: 2, title: "Second Post" }
            ]);
        }, 1000);
    });
}

// Using the async function
fetchUserData(123)
    .then(data => console.log("User data:", data))
    .catch(error => console.error("Failed:", error));

Synchronization Mechanisms

Synchronization mechanisms are used to coordinate the execution of concurrent threads and protect shared resources from race conditions.

1. Mutex (Mutual Exclusion)

A mutex is a locking mechanism that ensures only one thread can access a resource at a time.

Properties:

  • Exclusive Access: Only one thread can hold the lock at a time
  • Blocking: Threads wait until the mutex is available
  • Ownership: The thread that locks the mutex must unlock it
  • Use Case: Protecting critical sections of code
// C++ example using mutex
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>

std::mutex mtx;
int counter = 0;

void increment(int iterations) {
    for (int i = 0; i < iterations; i++) {
        // Lock the mutex before accessing shared resource
        mtx.lock();
        counter++;
        // Unlock the mutex after accessing shared resource
        mtx.unlock();
    }
}

int main() {
    const int numThreads = 10;
    const int iterationsPerThread = 1000;
    
    std::vector<std::thread> threads;
    
    // Create and start threads
    for (int i = 0; i < numThreads; i++) {
        threads.push_back(std::thread(increment, iterationsPerThread));
    }
    
    // Wait for all threads to complete
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "Final counter value: " << counter << std::endl;
    // Expected output: 10,000
    
    return 0;
}

2. Semaphores

Semaphores are signaling mechanisms that control access to a shared resource by multiple threads.

Types:

  • Binary Semaphore: Similar to a mutex, allows only one thread at a time
  • Counting Semaphore: Allows a fixed number of threads to access a resource

Operations:

  • Wait (P): Decrements the semaphore value, blocks if value is zero
  • Signal (V): Increments the semaphore value, potentially unblocking waiting threads
# Python example using semaphores
import threading
import time

# Create a semaphore that allows 2 concurrent accesses
semaphore = threading.Semaphore(2)

def access_resource(thread_id):
    print(f"Thread {thread_id} is trying to access the resource")
    
    # Acquire the semaphore
    semaphore.acquire()
    
    try:
        print(f"Thread {thread_id} has accessed the resource")
        # Simulate using the resource
        time.sleep(2)
        print(f"Thread {thread_id} is releasing the resource")
    finally:
        # Release the semaphore
        semaphore.release()

# Create and start 5 threads
threads = []
for i in range(5):
    t = threading.Thread(target=access_resource, args=(i,))
    threads.append(t)
    t.start()

# Wait for all threads to complete
for t in threads:
    t.join()

print("All threads have completed")

3. Condition Variables

Condition variables allow threads to wait for a specific condition to become true before proceeding.

Operations:

  • Wait: Releases the associated mutex and blocks until signaled
  • Signal/Notify: Wakes up one waiting thread
  • Broadcast/NotifyAll: Wakes up all waiting threads

Use Cases:

  • Producer-Consumer: Producers wait when buffer is full, consumers wait when buffer is empty
  • Thread Coordination: Threads wait for specific events or conditions
// Java example of producer-consumer using condition variables
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ProducerConsumer {
    private final Queue<Integer> buffer = new LinkedList<>();
    private final int capacity = 5;
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    
    public void produce(int item) throws InterruptedException {
        lock.lock();
        try {
            // Wait until buffer is not full
            while (buffer.size() == capacity) {
                System.out.println("Buffer is full, producer waiting...");
                notFull.await();
            }
            
            // Add item to buffer
            buffer.add(item);
            System.out.println("Produced: " + item);
            
            // Signal that buffer is not empty
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }
    
    public int consume() throws InterruptedException {
        lock.lock();
        try {
            // Wait until buffer is not empty
            while (buffer.isEmpty()) {
                System.out.println("Buffer is empty, consumer waiting...");
                notEmpty.await();
            }
            
            // Remove item from buffer
            int item = buffer.remove();
            System.out.println("Consumed: " + item);
            
            // Signal that buffer is not full
            notFull.signal();
            
            return item;
        } finally {
            lock.unlock();
        }
    }
}

4. Read-Write Locks

Read-write locks allow multiple readers to access a resource simultaneously, but only one writer at a time.

Properties:

  • Multiple Readers: Many threads can read simultaneously
  • Exclusive Writers: Only one thread can write at a time
  • No Readers During Write: No thread can read while a thread is writing
  • Use Case: Data structures that are read frequently but updated infrequently

5. Atomic Operations

Atomic operations are executed as a single, indivisible unit, without the possibility of interference from other threads.

Common Atomic Operations:

  • Read-Modify-Write: Operations like increment, compare-and-swap
  • Load/Store: Reading or writing a value atomically
  • Advantages: No need for explicit locks, better performance
  • Limitations: Limited to simple operations on individual variables
// Java example using AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class AtomicExample {
    // Using AtomicInteger instead of regular int
    private static AtomicInteger counter = new AtomicInteger(0);
    
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        for (int i = 0; i < 1000; i++) {
            executor.submit(() -> {
                // Atomic increment operation
                counter.incrementAndGet();
            });
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        
        System.out.println("Final counter value: " + counter.get());
        // Expected output: 1000
    }
}

Concurrency Patterns

Concurrency patterns are reusable solutions to common problems in concurrent programming. They help structure concurrent code in a way that is safe, efficient, and maintainable.

1. Producer-Consumer Pattern

The producer-consumer pattern involves two types of threads: producers that create data items and add them to a shared buffer, and consumers that remove and process these items.

Components:

  • Producers: Generate data and add it to the buffer
  • Consumers: Take data from the buffer and process it
  • Buffer: Shared data structure with synchronization mechanisms

Applications:

  • Task Queues: Workers consuming tasks from a queue
  • Event Processing: Event generators and handlers
  • Data Pipelines: Multi-stage data processing

2. Reader-Writer Pattern

The reader-writer pattern allows concurrent read access to a resource while ensuring exclusive write access.

Variations:

  • Reader Preference: Prioritizes read operations, may lead to writer starvation
  • Writer Preference: Prioritizes write operations, ensures data consistency
  • Fair Policy: Balances access between readers and writers

Applications:

  • Caches: Multiple threads reading cached data, occasional updates
  • Databases: Concurrent read transactions with occasional write transactions
  • Configuration: Frequently read configuration with occasional updates

3. Thread Pool Pattern

The thread pool pattern maintains a pool of worker threads to execute tasks, avoiding the overhead of thread creation and destruction.

Components:

  • Task Queue: Holds tasks waiting to be executed
  • Worker Threads: Execute tasks from the queue
  • Thread Pool Manager: Creates and manages worker threads

Types:

  • Fixed Thread Pool: Constant number of threads
  • Cached Thread Pool: Creates new threads as needed, reuses idle threads
  • Scheduled Thread Pool: Executes tasks after a delay or periodically
  • Work-Stealing Pool: Threads can steal tasks from other threads' queues

4. Future/Promise Pattern

The future/promise pattern represents a value that may not be available yet but will be resolved at some point in the future.

Components:

  • Future/Promise: Placeholder for a value that will be available later
  • Producer: Computes the value and fulfills the promise
  • Consumer: Uses the value when it becomes available

Operations:

  • Creation: Create a future for an asynchronous operation
  • Completion: Set the result or exception
  • Waiting: Block until the result is available
  • Callbacks: Register functions to be called when the future completes
  • Chaining: Compose futures to create pipelines of asynchronous operations
// Java example using CompletableFuture
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class FutureExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Create a CompletableFuture
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // Simulate a long-running task
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello, World!";
        });
        
        // Add a callback to be executed when the future completes
        future.thenAccept(result -> System.out.println("Got result: " + result));
        
        // Chain another operation
        CompletableFuture<String> transformedFuture = future.thenApply(result -> result.toUpperCase());
        
        // Wait for the result
        String result = transformedFuture.get();
        System.out.println("Transformed result: " + result);
    }
}

5. Actor Model

The actor model is a conceptual model for concurrent computation where "actors" are the universal primitives of concurrent computation.

Characteristics:

  • Encapsulation: Actors encapsulate state and behavior
  • Message Passing: Actors communicate by sending messages
  • No Shared State: Actors don't share mutable state
  • Asynchronous: Message processing is asynchronous

Implementations:

  • Akka: Actor framework for Java/Scala
  • Erlang/Elixir: Languages built around the actor model
  • Orleans: Microsoft's virtual actor framework

Deadlocks & Race Conditions

Deadlocks and race conditions are common concurrency problems that can lead to program failures or incorrect results.

1. Deadlocks

A deadlock occurs when two or more threads are blocked forever, each waiting for resources held by the others.

Conditions for Deadlock:

  • Mutual Exclusion: At least one resource must be held in a non-sharable mode
  • Hold and Wait: A thread holds at least one resource while waiting for additional resources
  • No Preemption: Resources cannot be forcibly taken from a thread
  • Circular Wait: A circular chain of threads, each waiting for a resource held by the next thread

Deadlock Prevention:

  • Resource Ordering: Acquire resources in a consistent order
  • Timeouts: Use timeouts when acquiring locks
  • Deadlock Detection: Detect and recover from deadlocks
  • Lock-Free Algorithms: Use non-blocking synchronization mechanisms
// Java example of a potential deadlock
public class DeadlockExample {
    private static final Object resource1 = new Object();
    private static final Object resource2 = new Object();
    
    public static void main(String[] args) {
        // Thread 1: Tries to lock resource1 then resource2
        Thread thread1 = new Thread(() -> {
            synchronized (resource1) {
                System.out.println("Thread 1: Locked resource 1");
                
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                // Try to lock resource2
                synchronized (resource2) {
                    System.out.println("Thread 1: Locked resource 2");
                }
            }
        });
        
        // Thread 2: Tries to lock resource2 then resource1
        Thread thread2 = new Thread(() -> {
            synchronized (resource2) {
                System.out.println("Thread 2: Locked resource 2");
                
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                // Try to lock resource1
                synchronized (resource1) {
                    System.out.println("Thread 2: Locked resource 1");
                }
            }
        });
        
        thread1.start();
        thread2.start();
    }
}

2. Race Conditions

A race condition occurs when the behavior of a program depends on the relative timing of events, such as the order in which threads execute.

Types of Race Conditions:

  • Data Race: Concurrent access to shared data without proper synchronization
  • Check-Then-Act: Condition checked, then action taken based on condition, but condition may change between check and act
  • Read-Modify-Write: Reading a value, modifying it, and writing it back without atomicity

Prevention:

  • Synchronization: Use locks, semaphores, or other synchronization mechanisms
  • Atomic Operations: Use atomic variables or operations
  • Immutable Data: Use immutable objects that cannot be modified after creation
  • Thread Confinement: Restrict data access to a single thread
// Java example of a race condition
public class RaceConditionExample {
    private static int counter = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                // Race condition: read-modify-write operation is not atomic
                counter++;
            }
        });
        
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter++;
            }
        });
        
        thread1.start();
        thread2.start();
        
        thread1.join();
        thread2.join();
        
        System.out.println("Counter value: " + counter);
        // Expected: 20000, but may be less due to race condition
    }
}

3. Livelock

A livelock occurs when threads are actively performing operations but not making progress because they keep responding to each other's actions.

Characteristics:

  • Active Threads: Unlike deadlock, threads are not blocked
  • No Progress: Threads keep changing state without making progress
  • Mutual Response: Each thread responds to the action of another thread

Prevention:

  • Randomization: Add randomness to retry logic
  • Timeouts: Use timeouts to break cycles
  • Prioritization: Assign priorities to threads

4. Starvation

Starvation occurs when a thread is unable to gain regular access to shared resources and is unable to make progress.

Causes:

  • Priority Scheduling: High-priority threads always execute first
  • Greedy Threads: Some threads hold resources for extended periods
  • Unfair Locks: No guarantee of fairness in lock acquisition

Prevention:

  • Fair Locks: Use locks that guarantee fairness
  • Resource Limits: Limit how long a thread can hold a resource
  • Priority Aging: Gradually increase the priority of waiting threads

Parallel Algorithms

Parallel algorithms are designed to take advantage of multiple processors or cores to solve problems more efficiently than sequential algorithms.

1. Parallel Sorting

Sorting algorithms that can be parallelized to improve performance on multi-core systems.

Algorithms:

  • Parallel Merge Sort: Divide the array, sort subarrays in parallel, then merge
  • Parallel Quicksort: Partition the array, then process partitions in parallel
  • Bitonic Sort: Designed specifically for parallel execution
// Java example of parallel merge sort
import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;

public class ParallelMergeSort {
    private static class SortTask extends RecursiveAction {
        private final int[] array;
        private final int start;
        private final int end;
        private final int threshold = 1000; // Threshold for parallel execution
        
        public SortTask(int[] array, int start, int end) {
            this.array = array;
            this.start = start;
            this.end = end;
        }
        
        @Override
        protected void compute() {
            if (end - start <= threshold) {
                // Use sequential sort for small arrays
                Arrays.sort(array, start, end);
            } else {
                // Split the array and sort in parallel
                int mid = start + (end - start) / 2;
                
                SortTask leftTask = new SortTask(array, start, mid);
                SortTask rightTask = new SortTask(array, mid, end);
                
                invokeAll(leftTask, rightTask);
                
                // Merge the sorted subarrays
                merge(array, start, mid, end);
            }
        }
        
        private void merge(int[] array, int start, int mid, int end) {
            int[] temp = new int[end - start];
            int i = start, j = mid, k = 0;
            
            while (i < mid && j < end) {
                if (array[i] <= array[j]) {
                    temp[k++] = array[i++];
                } else {
                    temp[k++] = array[j++];
                }
            }
            
            while (i < mid) {
                temp[k++] = array[i++];
            }
            
            while (j < end) {
                temp[k++] = array[j++];
            }
            
            System.arraycopy(temp, 0, array, start, temp.length);
        }
    }
    
    public static void parallelSort(int[] array) {
        ForkJoinPool pool = ForkJoinPool.commonPool();
        pool.invoke(new SortTask(array, 0, array.length));
    }
}

2. Parallel Graph Algorithms

Graph algorithms that can be parallelized to process large graphs more efficiently.

Algorithms:

  • Parallel Breadth-First Search: Process nodes at the same level in parallel
  • Parallel Dijkstra's Algorithm: Process multiple vertices in parallel
  • Parallel PageRank: Compute page ranks in parallel
  • Parallel Connected Components: Find connected components in parallel

3. Parallel Matrix Operations

Matrix operations that can be parallelized to improve performance for large matrices.

Operations:

  • Matrix Multiplication: Divide into blocks and multiply in parallel
  • Matrix Transposition: Transpose blocks in parallel
  • Matrix Decomposition: Parallel LU, QR, or Cholesky decomposition

4. Map-Reduce

A programming model for processing and generating large datasets with a parallel, distributed algorithm.

Phases:

  • Map: Process input data and produce intermediate key-value pairs
  • Shuffle: Group intermediate data by key
  • Reduce: Process each group of data in parallel

Applications:

  • Word Count: Count occurrences of words in documents
  • Inverted Index: Build search indexes
  • Graph Processing: Process large graphs
  • Machine Learning: Train models on large datasets
// Java example of a simple map-reduce for word count
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class MapReduceWordCount {
    public static void main(String[] args) throws InterruptedException {
        String[] documents = {
            "Hello world hello",
            "World is beautiful",
            "Hello beautiful world"
        };
        
        // Map phase: Count words in each document
        List<Map<String, Integer>> mapResults = new ArrayList<>();
        ExecutorService mapPool = Executors.newFixedThreadPool(documents.length);
        
        for (String document : documents) {
            mapPool.submit(() -> {
                Map<String, Integer> wordCount = new HashMap<>();
                String[] words = document.toLowerCase().split("\s+");
                
                for (String word : words) {
                    wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
                }
                
                synchronized (mapResults) {
                    mapResults.add(wordCount);
                }
            });
        }
        
        mapPool.shutdown();
        mapPool.awaitTermination(1, TimeUnit.MINUTES);
        
        // Shuffle phase: Group by key
        Map<String, List<Integer>> shuffleResults = new HashMap<>();
        
        for (Map<String, Integer> result : mapResults) {
            for (Map.Entry<String, Integer> entry : result.entrySet()) {
                String word = entry.getKey();
                Integer count = entry.getValue();
                
                shuffleResults.computeIfAbsent(word, k -> new ArrayList<>()).add(count);
            }
        }
        
        // Reduce phase: Sum counts for each word
        Map<String, Integer> reduceResults = new ConcurrentHashMap<>();
        ExecutorService reducePool = Executors.newFixedThreadPool(shuffleResults.size());
        
        for (Map.Entry<String, List<Integer>> entry : shuffleResults.entrySet()) {
            String word = entry.getKey();
            List<Integer> counts = entry.getValue();
            
            reducePool.submit(() -> {
                int sum = counts.stream().mapToInt(Integer::intValue).sum();
                reduceResults.put(word, sum);
            });
        }
        
        reducePool.shutdown();
        reducePool.awaitTermination(1, TimeUnit.MINUTES);
        
        // Print results
        System.out.println("Word counts:");
        reduceResults.forEach((word, count) -> 
            System.out.println(word + ": " + count));
    }
}

5. Parallel Prefix Sum (Scan)

Efficiently compute the prefix sum of an array in parallel.

Algorithm:

  • Up-Sweep Phase: Build a reduction tree
  • Down-Sweep Phase: Distribute values back down the tree
  • Time Complexity: O(log n) with n processors

Applications:

  • Stream Compaction: Remove elements that don't satisfy a predicate
  • Radix Sort: Efficient parallel sorting
  • Histogram: Compute histograms in parallel

Practice Problems

Test your understanding of concurrency and parallelism concepts with these practice problems.

1. Dining Philosophers Problem

MediumDeadlock

Implement a solution to the dining philosophers problem that avoids deadlock and starvation.

View Problem →

2. Thread-Safe Queue

MediumSynchronization

Implement a thread-safe queue with blocking operations for producer-consumer scenarios.

View Problem →

3. Parallel Web Crawler

HardConcurrency

Implement a parallel web crawler that can efficiently crawl multiple web pages concurrently.

View Problem →

4. Parallel Matrix Multiplication

MediumParallelism

Implement a parallel algorithm for matrix multiplication that efficiently utilizes multiple cores.

View Problem →

5. Thread Pool Implementation

HardThread Management

Implement a custom thread pool with work stealing and task prioritization.

View Problem →

6. Readers-Writers Lock

MediumSynchronization

Implement a readers-writers lock that allows multiple readers or a single writer.

View Problem →

7. Parallel Merge Sort

MediumParallelism

Implement a parallel version of merge sort that efficiently utilizes multiple cores.

View Problem →

8. Concurrent Hash Map

HardData Structures

Implement a thread-safe hash map that allows concurrent reads and writes.

View Problem →