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'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)
Each process has its own Python interpreter and memory space, bypassing the GIL.
Python: asyncio (Async I/O)
Python: concurrent.futures (Unified API)
JavaScript: Promise.all
Go: Goroutines
Choosing the Right Approach
| Workload | Python | JavaScript | Go | Java |
| I/O-bound (network, disk) | threading or asyncio | Promise.all | Goroutines | CompletableFuture |
| CPU-bound (computation) | multiprocessing | Worker threads | Goroutines | ExecutorService |
| Mixed I/O + CPU | concurrent.futures | Worker + Promise | Goroutines | Virtual 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
multiprocessingorProcessPoolExecutorinstead. Threading only helps for I/O-bound tasks where the GIL is released. - Not joining threads or awaiting futures: Forgetting
thread.join()orawait 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
Processcopies the entire Python interpreter. Spawning hundreds of processes wastes memory and CPU on context switching. Use aPoolwithmax_workersmatching your CPU core count (os.cpu_count()). - Mixing asyncio with blocking calls: Calling
time.sleep()orrequests.get()inside anasyncfunction blocks the event loop and prevents other coroutines from running. Useawait asyncio.sleep()andaiohttpinstead, or run blocking code inasyncio.to_thread().
Summary
- Use
threadingorasynciofor I/O-bound tasks (network requests, file I/O, database queries) - Use
multiprocessingfor CPU-bound tasks (number crunching, image processing, ML training) concurrent.futuresprovides a unified API for both thread and process pools- JavaScript uses
Promise.allfor parallel async operations; Go uses goroutines withsync.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

