Concurrency & Parallelism | Early Computing to Modern Cloud Architecture

Vikesh Mittal
7 min readSep 21, 2024

--

The concepts of concurrency and parallelism have roots that trace back to the earliest days of computing, evolving alongside advancements in hardware and software design. I will try to provide some historical context to the concepts and will try not to make you sleepy while reading it. This context will also help understand how these concepts are applied in modern computing environments like AWS.

Concurrency: From Time-Sharing to Multi-Threading

Concurrency is fundamentally about the management of multiple tasks within a system, allowing them to progress at overlapping times, even if they aren’t executed simultaneously. This concept emerged in the early days of computing, particularly with the development of time-sharing systems in the 1960s.

Time-Sharing Systems

In the 1960s, the arrival of time-sharing operating systems marked a significant milestone in the evolution of concurrency.

Time-sharing allowed multiple users to interact with a single computer system simultaneously by rapidly switching between tasks.

This concept was pivotal in making computers more accessible and efficient, laying the groundwork for modern multitasking and concurrent programming.

Multi-Threading

As hardware evolved, particularly with the introduction of multi-core processors, the concept of concurrency expanded into multi-threading.

Multi-threading allows a single program to manage multiple threads of execution, effectively performing different tasks within the same application concurrently.

This development became particularly important in the 1980s and 1990s, as desktop computing and real-time systems demanded more efficient processing capabilities.

Parallelism: Leveraging Hardware for Simultaneous Execution

Parallelism, in contrast, is concerned with the simultaneous execution of multiple tasks, a concept that gained prominence as computing moved from single-core to multi-core processors.

Early Experiments in Parallel Computing

Parallelism’s origins can be traced back to the 1950s and 1960s, with early experiments in parallel processing architectures. One notable example is the ILLIAC IV project, developed at the University of Illinois.

ILLIAC IV, completed in 1976, was one of the first supercomputers designed to execute multiple instructions simultaneously, using an array of processors working in parallel.

Although it faced numerous challenges and delays, the ILLIAC IV was a pioneering effort that demonstrated the potential of parallel processing in scientific computing.

The Rise of Multi-Core Processors

The true acceleration of parallelism in computing came with the advent of multi-core processors in the 2000s.

These processors, which contain multiple processing units on a single chip, allow for the parallel execution of tasks, significantly improving the performance of applications designed to take advantage of this architecture.

This era marked a shift from simply managing tasks concurrently to executing them in parallel, optimizing computing power and efficiency.

Concurrency vs. Parallelism

Concurrency refers to the ability of a system to handle multiple tasks simultaneously in terms of their lifecycle. However, these tasks do not necessarily execute at the same time. Instead, the system switches between tasks, allowing them to progress during overlapping time periods. Concurrency is more about the management of tasks, ensuring that multiple operations can be performed seemingly simultaneously.

(Example) Concurrent API Requests in TypeScript

// Simulating asynchronous operations (e.g., API calls) with different response times.
const fetchDataFromAPI = (id: number, delay: number): Promise<string> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(`Data from API ${id} after ${delay}ms`);
}, delay);
});
};

// Function to simulate concurrent requests
const simulateConcurrentRequests = async () => {
// Concurrently starting multiple asynchronous tasks (API requests)
const tasks = [
fetchDataFromAPI(1, 2000), // Task 1 takes 2 seconds to resolve
fetchDataFromAPI(2, 1000), // Task 2 takes 1 second to resolve
fetchDataFromAPI(3, 3000), // Task 3 takes 3 seconds to resolve
];

// Using Promise.all to run all tasks concurrently
const results = await Promise.all(tasks);

console.log("All tasks completed:");
results.forEach((result, index) => {
console.log(`Task ${index + 1}: ${result}`);
});
};

// Call the function to see concurrency in action
simulateConcurrentRequests();

// Output
/*
All tasks completed:
Task 1: Data from API 2 after 1000ms
Task 2: Data from API 1 after 2000ms
Task 3: Data from API 3 after 3000ms
*/

Parallelism, on the other hand, involves the simultaneous execution of multiple tasks. This requires multiple processors or cores to work on different tasks at the same time. Parallelism is about improving performance by reducing the time required to complete tasks, as different parts of the workload are processed simultaneously.

(Example) Parallelism Using Worker Threads

/**
* worker.ts
*/
import { parentPort, workerData } from 'worker_threads';

// Simulating a CPU-intensive task (e.g., calculating Fibonacci numbers)
const fibonacci = (n: number): number => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
};

// Perform the computation and send the result back to the main thread
const result = fibonacci(workerData);
parentPort?.postMessage(result);
/**
* main.ts
*/
import { Worker } from 'worker_threads';

// This function spawns a new worker thread to execute a computational task
// in parallel. It uses Node.js worker_threads to offload the calculation
// to a separate thread.
const runParallelTask = (workerData: number): Promise<number> => {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
};

// This function creates multiple workers, each working on a separate task
// (in this case, computing the Fibonacci sequence). The tasks are executed
// in parallel across multiple CPU threads.
const runParallelTasks = async () => {
console.log('Starting parallel tasks...');

const taskData = [30, 35, 40]; // Input data for the tasks (simulating intensive calculations)

// Start multiple tasks in parallel
const tasks = taskData.map(data => runParallelTask(data));

// Wait for all tasks to finish
const results = await Promise.all(tasks);

console.log('Parallel tasks completed:');
results.forEach((result, index) => {
console.log(`Result from task ${index + 1}: ${result}`);
});
};

// Call the function to see parallelism in action
runParallelTasks();

Concurrency and Parallelism in AWS

AWS offers a robust set of tools and services that allow us to implement concurrency and parallelism as we need. Let’s explore using some real-world examples on how these concepts are applied in AWS.

Concurrency in AWS: Efficient Task Management

In AWS, concurrency can be effectively managed using services like Lambda, Step Functions, and Simple Queue Service (SQS). These services allow for the handling of multiple tasks simultaneously, optimizing resource usage and improving system responsiveness.

(Example) Lambda with Reserved Concurrency → Consider a scenario where an application needs to process incoming requests rapidly and concurrently. Lambda is an ideal service for this purpose, as it can handle multiple requests concurrently by invoking separate instances of a function. For instance, if an application needs to process thousands of user requests per second, Lambda can scale up by running multiple function instances simultaneously. By setting Reserved Concurrency, you can ensure that a specific number of concurrent executions are always available, preventing bottlenecks and ensuring critical requests are processed without delay.

Parallelism in AWS: Simultaneous Task Execution

Parallelism in AWS is achieved by distributing tasks across multiple resources, such as EC2 instances, ECS containers, or leveraging parallel processing capabilities in services like AWS Batch or AWS Glue.

(Example) Parallel Processing with AWS EC2 Auto Scaling → Imagine a scenario where you need to process a large dataset, such as a video rendering task or a big data job. EC2 Auto Scaling allows you to automatically scale the number of EC2 instances to process different parts of the dataset simultaneously. By distributing the workload across multiple instances, each instance processes a subset of the data in parallel, significantly reducing overall processing time.

Combining Concurrency and Parallelism in AWS

Concurrent but Not Parallel

AWS Lambda functions handle multiple requests concurrently but execute each request sequentially within its own invocation. For example, user requests are sent to an API Gateway, which triggers Lambda functions that run concurrently, with outputs saved to an Amazon S3 bucket.

Parallel but Not Concurrent

Data processing tasks are distributed across multiple EC2 instances using an Auto Scaling group, where each instance processes a different chunk of data in parallel, but no instance handles multiple tasks concurrently. For example, data chunks are processed in parallel by EC2 instances within an Auto Scaling group, with results stored in Amazon S3.

Concurrent and Parallel

A complex workflow managed by AWS Step Functions orchestrates tasks that are executed concurrently and in parallel across multiple Lambda functions and EC2 instances. For example, step functions orchestrate a workflow involving both Lambda and EC2 instances, with outputs saved to Amazon S3 or Amazon Redshift.

Neither Concurrent nor Parallel

A simple, sequential task such as a backup script running on a single EC2 instance. The task is executed sequentially on an EC2 instance, with backup data stored in Amazon S3.

As someone who works closely with modern cloud architectures and computational tasks, I find that understanding the distinctions between concurrency and parallelism is crucial in building efficient and scalable systems. Whether I’m leveraging AWS services to handle concurrent requests or utilizing worker threads for parallel processing, these concepts help me optimize application performance in different scenarios. I hope this exploration gave you a clear perspective on how you can approach these concepts in your own projects. The key is knowing when to apply each strategy to maximize the efficiency and responsiveness of your applications.

--

--

Vikesh Mittal
Vikesh Mittal

Written by Vikesh Mittal

User Interface Architect | Passionate about Micro-frontends | Angular | React | https://vikeshmittal.com/

No responses yet