Parallel Execution
Concurrency
Multithreading
Asynchronous Programming
Function Running

How to run multiple functions at the same time?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Running multiple functions simultaneously can be achieved through threading, multiprocessing, or async/await, depending on whether the workload is I/O-bound or CPU-bound. In Python, threading runs functions concurrently on a single core (best for I/O-bound tasks), multiprocessing runs them in parallel on multiple cores (best for CPU-bound tasks), and asyncio provides cooperative concurrency for async I/O operations. Other languages like JavaScript, Go, and Java each have their own concurrency models.

Python: Threading (I/O-Bound)

python
1import threading
2import time
3
4def download(url):
5    print(f"Downloading {url}...")
6    time.sleep(2)  # Simulate network I/O
7    print(f"Finished {url}")
8
9def process_data(data):
10    print(f"Processing {data}...")
11    time.sleep(3)
12    print(f"Finished processing {data}")
13
14# Create threads
15t1 = threading.Thread(target=download, args=("https://example.com",))
16t2 = threading.Thread(target=process_data, args=("dataset.csv",))
17
18# Start both
19t1.start()
20t2.start()
21
22# Wait for both to finish
23t1.join()
24t2.join()
25print("All tasks complete")  # Runs after ~3 seconds, not 5

Python's GIL (Global Interpreter Lock) means threads do not run Python code in parallel, but they do release the GIL during I/O operations, so I/O-bound tasks benefit from threading.

Python: Multiprocessing (CPU-Bound)

python
1from multiprocessing import Process, Pool
2import os
3
4def compute_heavy(n):
5    """CPU-intensive calculation"""
6    result = sum(i * i for i in range(n))
7    print(f"PID {os.getpid()}: result = {result}")
8    return result
9
10# Option 1: Process objects
11p1 = Process(target=compute_heavy, args=(10_000_000,))
12p2 = Process(target=compute_heavy, args=(20_000_000,))
13p1.start()
14p2.start()
15p1.join()
16p2.join()
17
18# Option 2: Pool for mapping over inputs
19with Pool(processes=4) as pool:
20    results = pool.map(compute_heavy, [1_000_000, 2_000_000, 3_000_000, 4_000_000])
21    print(results)

Each process has its own Python interpreter and memory space, bypassing the GIL.

Python: asyncio (Async I/O)

python
1import asyncio
2import aiohttp
3
4async def fetch(session, url):
5    async with session.get(url) as response:
6        data = await response.text()
7        print(f"Fetched {url}: {len(data)} chars")
8        return data
9
10async def compute(name, duration):
11    print(f"Starting {name}")
12    await asyncio.sleep(duration)
13    print(f"Finished {name}")
14    return name
15
16async def main():
17    # Run multiple async functions concurrently
18    results = await asyncio.gather(
19        compute("task_a", 2),
20        compute("task_b", 3),
21        compute("task_c", 1),
22    )
23    print(results)  # ['task_a', 'task_b', 'task_c'] after ~3 seconds
24
25asyncio.run(main())

Python: concurrent.futures (Unified API)

python
1from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
2
3def task(name, duration):
4    import time
5    time.sleep(duration)
6    return f"{name} done"
7
8# ThreadPoolExecutor for I/O-bound work
9with ThreadPoolExecutor(max_workers=4) as executor:
10    futures = {
11        executor.submit(task, "download", 2): "download",
12        executor.submit(task, "upload", 3): "upload",
13        executor.submit(task, "query", 1): "query",
14    }
15    for future in as_completed(futures):
16        print(future.result())
17
18# ProcessPoolExecutor for CPU-bound work — same API
19with ProcessPoolExecutor(max_workers=4) as executor:
20    results = list(executor.map(compute_heavy, [1_000_000, 2_000_000]))

JavaScript: Promise.all

javascript
1async function fetchData(url) {
2  const response = await fetch(url);
3  return response.json();
4}
5
6async function processAll() {
7  // Run all three in parallel
8  const [users, posts, comments] = await Promise.all([
9    fetchData("/api/users"),
10    fetchData("/api/posts"),
11    fetchData("/api/comments"),
12  ]);
13  console.log(users, posts, comments);
14}
15
16// Promise.allSettled — does not reject on individual failures
17const results = await Promise.allSettled([
18  fetchData("/api/users"),
19  fetchData("/api/broken"),  // This fails but doesn't kill the others
20]);

Go: Goroutines

go
1package main
2
3import (
4    "fmt"
5    "sync"
6    "time"
7)
8
9func download(url string, wg *sync.WaitGroup) {
10    defer wg.Done()
11    fmt.Printf("Downloading %s...\n", url)
12    time.Sleep(2 * time.Second)
13    fmt.Printf("Finished %s\n", url)
14}
15
16func main() {
17    var wg sync.WaitGroup
18
19    urls := []string{"url1", "url2", "url3"}
20    for _, url := range urls {
21        wg.Add(1)
22        go download(url, &wg)
23    }
24
25    wg.Wait()
26    fmt.Println("All downloads complete")
27}

Choosing the Right Approach

WorkloadPythonJavaScriptGoJava
I/O-bound (network, disk)threading or asyncioPromise.allGoroutinesCompletableFuture
CPU-bound (computation)multiprocessingWorker threadsGoroutinesExecutorService
Mixed I/O + CPUconcurrent.futuresWorker + PromiseGoroutinesVirtual threads (Java 21+)

Common Pitfalls

  • Using Python threading for CPU-bound work: The GIL prevents threads from running Python bytecode in parallel. CPU-bound tasks gain no speedup from threading — use multiprocessing or ProcessPoolExecutor instead. Threading only helps for I/O-bound tasks where the GIL is released.
  • Not joining threads or awaiting futures: Forgetting thread.join() or await asyncio.gather() causes the main program to exit before workers finish. Always wait for all concurrent tasks to complete before proceeding or exiting.
  • Shared mutable state without synchronization: Multiple threads modifying the same variable without a lock causes race conditions. Use threading.Lock, queue.Queue, or design with immutable data. Multiprocessing avoids this by using separate memory spaces.
  • Creating too many processes: Each Process copies the entire Python interpreter. Spawning hundreds of processes wastes memory and CPU on context switching. Use a Pool with max_workers matching your CPU core count (os.cpu_count()).
  • Mixing asyncio with blocking calls: Calling time.sleep() or requests.get() inside an async function blocks the event loop and prevents other coroutines from running. Use await asyncio.sleep() and aiohttp instead, or run blocking code in asyncio.to_thread().

Summary

  • Use threading or asyncio for I/O-bound tasks (network requests, file I/O, database queries)
  • Use multiprocessing for CPU-bound tasks (number crunching, image processing, ML training)
  • concurrent.futures provides a unified API for both thread and process pools
  • JavaScript uses Promise.all for parallel async operations; Go uses goroutines with sync.WaitGroup
  • Always synchronize shared state with locks or use message passing to avoid race conditions
  • Match concurrency approach to the workload type — wrong choice yields no speedup or worse performance

Course illustration
Course illustration

All Rights Reserved.