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.
Module Contents
Learning Resources
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.
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
Implement a solution to the dining philosophers problem that avoids deadlock and starvation.
View Problem →2. Thread-Safe Queue
Implement a thread-safe queue with blocking operations for producer-consumer scenarios.
View Problem →3. Parallel Web Crawler
Implement a parallel web crawler that can efficiently crawl multiple web pages concurrently.
View Problem →4. Parallel Matrix Multiplication
Implement a parallel algorithm for matrix multiplication that efficiently utilizes multiple cores.
View Problem →5. Thread Pool Implementation
Implement a custom thread pool with work stealing and task prioritization.
View Problem →6. Readers-Writers Lock
Implement a readers-writers lock that allows multiple readers or a single writer.
View Problem →7. Parallel Merge Sort
Implement a parallel version of merge sort that efficiently utilizes multiple cores.
View Problem →8. Concurrent Hash Map
Implement a thread-safe hash map that allows concurrent reads and writes.
View Problem →