Types of Programming

Concurrent & Parallel Programming

Managing multiple computations that overlap in time and the hazards that come with it.

Overview

Concurrent programming structures programs as multiple overlapping computations that may share resources. It is distinct from parallelism: concurrency is about dealing with many things at once (structure); parallelism is about doing many things at once (execution). Languages address concurrency with threads, coroutines, actors, CSP channels, or async/await.

Origin

Dijkstra formalised concurrent processes and mutual exclusion in the 1960s (THE multiprogramming system, 1968). Hoare introduced CSP (Communicating Sequential Processes, 1978) and monitors. Tony Hoare's dining philosophers problem and Dijkstra's semaphores became foundational. Go's goroutines and channels are a direct CSP implementation.

Examples

Concurrent HTTP requests with Promise.all in TypeScript

interface UserProfile {
  id: number;
  name: string;
  email: string;
}

async function fetchUserBatch(ids: number[]): Promise<UserProfile[]> {
  const requests = ids.map(id =>
    fetch('/api/users/' + id).then(r => {
      if (!r.ok) throw new Error('HTTP ' + r.status + ' for user ' + id);
      return r.json() as Promise<UserProfile>;
    })
  );

  return Promise.all(requests);
}

// Fetches all three concurrently rather than sequentially
const profiles = await fetchUserBatch([1, 2, 3]);
// Wall-clock time: max(t1, t2, t3) rather than t1 + t2 + t3

Promise.all launches all requests before awaiting any. If any rejects, the whole batch rejects. Use Promise.allSettled to collect partial results when individual failures are acceptable.

Worker threads for CPU-bound tasks in Node.js

import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import { cpus } from 'os';

function computeHash(data: string): Promise<string> {
  return new Promise((resolve, reject) => {
    const worker = new Worker(__filename, {
      workerData: { input: data }
    });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

if (!isMainThread) {
  const crypto = require('crypto');
  const result = crypto
    .createHash('sha256')
    .update(workerData.input)
    .digest('hex');
  parentPort!.postMessage(result);
}

// In main thread
const cores = cpus().length;
const results = await Promise.all(
  Array.from({ length: cores }, (_, i) => computeHash('data-chunk-' + i))
);

Node.js worker_threads (stable since v12) run in separate V8 isolates with their own event loops. Use them for CPU-bound work that blocks the main event loop. SharedArrayBuffer allows zero-copy memory sharing between threads.

Use Cases

  • 01Web servers handling thousands of simultaneous connections without a thread-per-request model (Node.js, Go, Nginx)
  • 02Batch jobs that fan out work across CPU cores or remote services (image processing, report generation)
  • 03UI applications that offload background work to keep the main thread responsive (Web Workers for parsing, indexing)
  • 04Database connection pooling where multiple queries share a fixed pool of connections concurrently

When Not to Use

  • //Simple sequential scripts where concurrency adds complexity without throughput benefit
  • //When shared mutable state is unavoidable; concurrent access to shared state introduces races that are notoriously hard to reproduce and fix
  • //Lightweight I/O operations where async/await on a single thread is sufficient; thread overhead (8MB stack on Linux) is not justified

Technical Notes

  • Go's goroutines are multiplexed onto OS threads by the runtime (GOMAXPROCS controls thread count, defaults to logical CPUs since Go 1.5). A goroutine starts at 2KB of stack versus 1-8MB for an OS thread
  • The Erlang/OTP actor model uses lightweight processes (BEAM VM, millions per node) with isolated heaps and message-passing; no shared memory means no locks, enabling the nine-nines uptime of telecom systems
  • JavaScript's event loop processes one task at a time from the task queue, one microtask at a time from the microtask queue. Promises schedule microtasks; setTimeout schedules macro-tasks. This is why resolved promises run before the next setTimeout callback
  • Amdahl's Law limits parallel speedup: if 5% of a program is sequential, maximum speedup is 20x regardless of how many cores are added. Profiling serial bottlenecks before parallelising is essential