We have introduced multiprocessing and multithreading, the two most common approaches to implementing multitasking. Now, let’s discuss the advantages and disadvantages of these two methods.
First, to implement multitasking, we typically design a Master-Worker pattern: the Master is responsible for assigning tasks, and Workers are responsible for executing them. Therefore, in a multitasking environment, there is usually one Master and multiple Workers.
The biggest advantage of the multiprocessing model is high stability: if a child process crashes, it will not affect the main process or other child processes. (Of course, if the main process crashes, all processes will terminate—but the Master process only handles task allocation and has a low probability of crashing.) The well-known Apache web server originally adopted the multiprocessing model.
The disadvantages of multiprocessing are:
fork() call works reasonably well on Unix/Linux systems, process creation incurs significant overhead on Windows.The multithreading model is usually slightly faster than multiprocessing—but not by much. Its critical disadvantage is that a crash in any thread can directly bring down the entire process, as all threads share the process’s memory space. On Windows, if code executed by a thread encounters an error, you will often see a prompt like: “This program has performed an illegal operation and will be closed.” In reality, it is usually a single thread that has failed, but the OS forces the entire process to terminate.
On Windows, multithreading is more efficient than multiprocessing, so Microsoft’s IIS server uses the multithreading model by default. However, due to stability issues with multithreading, IIS is less stable than Apache. To mitigate this problem, both IIS and Apache now use a hybrid model of multiprocessing + multithreading—only making the architecture more complex.
Whether using multiprocessing or multithreading, performance will inevitably degrade when the number of tasks/threads becomes too large. Why?
Let’s use an analogy: suppose you are preparing for the high school entrance exam and need to complete homework for 5 subjects (Chinese, Math, English, Physics, Chemistry) every night, with each subject taking 1 hour.
However, switching tasks has a cost: switching from Chinese to Math requires first putting away your Chinese books and pens (saving the context), then opening your Math textbook and fetching a compass/ruler (preparing the new environment) before you can start. Operating systems follow the same logic when switching processes/threads: they must save the current execution context (CPU register state, memory pages, etc.), then restore the context of the new task (recover register state, switch memory pages, etc.) before execution can begin. While this switch is fast, it still consumes time. With thousands of concurrent tasks, the OS may spend most of its time switching tasks instead of executing them—resulting in symptoms like hard drive thrashing, unresponsive windows, and system freezes.
Thus, once the number of concurrent tasks exceeds a threshold, the system’s resources are fully consumed, leading to a sharp drop in efficiency and poor performance across all tasks.
The second consideration for using multitasking is the type of task, which falls into two categories: CPU-bound and I/O-bound.
Characterized by heavy computation and high CPU utilization (e.g., calculating pi, HD video decoding)—performance depends entirely on CPU processing power. While CPU-bound tasks can be executed via multitasking, more tasks mean more time spent on context switching and lower CPU efficiency. To utilize the CPU optimally, the number of concurrent CPU-bound tasks should equal the number of CPU cores.
Since CPU-bound tasks primarily consume CPU resources, code execution efficiency is critical. Scripting languages like Python have low runtime efficiency and are entirely unsuitable for CPU-bound tasks—C language is the best choice for such scenarios.
Tasks involving network/disk I/O (e.g., web applications) are I/O-bound. Their key characteristic is minimal CPU usage: most time is spent waiting for I/O operations to complete (I/O speed is far slower than CPU/memory speed). For I/O-bound tasks, more concurrent tasks improve CPU efficiency (up to a limit). Most common tasks (e.g., web apps) are I/O-bound.
During I/O-bound task execution, 99% of time is spent on I/O and only 1% on CPU. Replacing a slow scripting language like Python with a fast compiled language like C yields almost no performance improvement. For I/O-bound tasks, the best choice is the language with the highest development efficiency (least code), making scripting languages the first choice (C has the worst development efficiency for such tasks).
Given the massive speed gap between CPU and I/O, a single-process/single-thread model would block other tasks while waiting for I/O operations to complete—hence the need for multiprocessing or multithreading to support concurrent execution.
Modern operating systems have made significant improvements to I/O operations, with asynchronous I/O being the most impactful feature. By fully leveraging OS-provided asynchronous I/O, multitasking can be implemented with a single process/thread using an event-driven model. Nginx (a popular web server) uses this model: on a single-core CPU, it efficiently handles multiple tasks with a single process; on multi-core CPUs, it runs multiple processes (matching the number of CPU cores) to fully utilize hardware resources. Since the total number of processes is limited, OS scheduling remains highly efficient. Asynchronous I/O has become a major trend for implementing multitasking.
In Python, the single-threaded asynchronous programming model is called coroutines. With coroutine support, developers can write high-efficiency multitasking programs based on event-driven architecture—we will discuss coroutine implementation in later sections.