What is a Process?
A process is the smallest unit for resource allocation and scheduling, forming the foundation of the operating system’s structure. It serves as a container for threads (like a house with an empty shell that cannot move).
- A process is an entity with its own address space, generally including the text region (text region), data region (data region), and stack region (stack region).
- The text region stores code executed by the processor; the data region stores variables and dynamically allocated memory used during execution; the stack region stores instructions and local variables called during active processes.
- A process is an “executing program.” A program is a lifeless entity that becomes an active entity only when the operating system calls it.
What is a Thread
A thread is referred to as a lightweight process, being the smallest unit that the operating system can schedule for computation. Threads are contained within processes and serve as the actual processing units (like people living in a house, who can move).
- A standard thread consists of a thread ID, current instruction pointer (PC), register set, and stack. Additionally, a thread is an entity within a process that is independently scheduled and dispatched by the system.
- A thread does not own system resources; it only possesses a small amount of essential resources needed for execution. However, it can share all resources of its parent process with other threads in the same process.
Three States of a Thread
A thread can create and terminate another thread. Multiple threads within the same process can execute concurrently. Due to mutual constraints between threads, they exhibit interrupiveness during execution. Threads have three basic states: ready, blocked, and running.
- The ready state indicates that the thread has all conditions for execution logically and is waiting for a processor.
- The running state means the thread occupies the processor and is executing.
- The blocked state means the thread is waiting for an event (such as a semaphore), logically unable to execute.
- Every program must have at least one thread. If a program has only one thread, it is the program itself.
What is a Coroutine
- A coroutine, also called a microthread, can contain multiple coroutines within a single program, similar to how a process contains multiple threads. The scheduling of coroutines is completely controlled by the user.
- Coroutines have their own register context and stack. When switching between coroutine schedules, the register context and stack are saved in other locations, and restored when returning.
- Direct manipulation of the stack has almost no kernel switch overhead, allowing un-locked access to global variables, making context switches very fast.
- The blocking of coroutines and threads is fundamentally different. The suspension of a coroutine is entirely controlled by the program, while the blocked state of a thread is switched by the operating system’s kernel. Therefore, the overhead of coroutines is much smaller than that of threads.
Comparison
Differences Between Processes and Threads:
A process has its own exclusive address space; starting a new process requires the system to allocate an address space.
All threads within a process share the entire resources of the process, using the same address space. Therefore, switching between threads is much cheaper than switching between processes, and creating a thread is far less expensive than creating a process.
Communication between threads is more convenient; all threads in the same process share global variables and static variables.
Communication between processes requires third-party assistance.
A thread can only belong to one process and can access resources of that process.
When an operating system creates a process, it automatically applies a main thread or primary thread.
Use threads for I/O-intensive tasks or functions;
Use processes for CPU-intensive tasks or functions.
Differences Between Threads and Coroutines:
- A thread can have multiple coroutines, and a process can also have multiple coroutines. Thus, Python can utilize multi-core CPUs.
- Both threads and processes are synchronous mechanisms, while coroutines are asynchronous.
- Coroutines can retain the state of the previous call. When re-entering, it is equivalent to entering the state of the previous call.
We often say that Python’s multithreading is fake because no matter how many threads you start, you have as many CPUs as you want, Python will always allow only one thread to run at the same time.
Why is this so? It is mainly due to the existence of GIL. Details can be found at http://bayestalk.com/592?
Summary
At this point, it is not difficult to see that from a certain perspective, they three represent a relationship of granularity. Just like slicing vegetables,
- You use a Qinglong Yanyue dao to slice, which is definitely possible, but you should cut in large chunks.
- Using a vegetable knife, the granularity is just right for daily life.
- But if you are making vegetable handicrafts, you may need very small and sharp carving tools.
Take PCs as an example.
- A task is a process (Process), such as opening a browser starts a browser process, opening Notepad starts a Notepad process, opening two Notepads starts two Notepad processes, and opening Word starts a Word process.
- Some processes do not just do one thing at the same time, like Word, which can perform typing, spelling check, printing, etc., simultaneously. To do multiple things within a process, you need to run multiple “subtasks” simultaneously. These subtasks inside a process are called threads (Thread).
- Since each process must do at least one thing, a process must have at least one thread.
- Of course, complex processes like Word can have multiple threads, and multiple threads can execute simultaneously. The execution method of multi-threading is the same as that of multi-processes: the operating system quickly switches between threads, making each thread appear to run simultaneously. However, true simultaneous execution of multiple threads requires a multi-core CPU.
Most of the time, Python programs are single-task processes with only one thread. If we want to execute multiple tasks at the same time, how can we do it?
- One way is to start multiple processes, where each process has only one thread but can execute multiple tasks.
- Another method is to start a process and then start multiple threads within that process, allowing multiple threads to execute multiple tasks.
- Of course, there’s also a third method: starting multiple processes, each of which starts multiple threads. This allows for more simultaneous tasks, although this model is more complex and rarely used in practice.
Programs involving multi-threading and multi-processing involve synchronization and data sharing issues, making them more complex to write.
Multi-threaded Programming
Problems with Multi-threaded Programming
import time, threading
# Assume this is your bank balance:
balance = 0
def change_it(n):
# First deposit then withdraw, the result should be 0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(1000000): # Set the number of iterations to a sufficiently large value.
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
run_thread(1)
print(balance)
Explained with a single-threaded approach, it’s like two people playing a very boring game, A gives B 1 block, B gives A 1 block, repeating countless times. So after billions of years, their wealth will not be affected by this game.
Initial balance = 0
change_it(n) function
- balance + 1
- balance - 1
One addition and one subtraction cancel each other out
Therefore, the final balance
However, in multi-threading, all variables are shared by all threads. Any variable can be modified by any thread. The biggest danger of shared data between threads is that multiple threads may modify the same variable at the same time.
How to Solve It?
import time, threading
# Assume this is your bank balance:
balance = 0
def change_it(n):
# First deposit then withdraw, the result should be 0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(10000000): # Set the number of iterations to a sufficiently large value.
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
run_thread(1)
print(balance)
The result is always 0. Different threads are isolated from each other by locks, and they do not interfere with each other.
threading Class
Python’s standard library provides two modules: _thread and threading, _thread is a low-level module, threading is an advanced module that wraps _thread. Most of the time, we only need to use the advanced threading module.
Starting a thread involves passing a function into it and creating a Thread instance, then calling start() to begin execution:
Other
- Suppose you are running a property management company. Initially, the business volume is small, and everything needs you to do it yourself—fixing the heating pipe for Old Zhang, immediately going to Old Li’s house to replace the light bulb—this is called single-threaded, with all work executed sequentially.
- Later, as the business expands, you hire several workers, allowing your property management company to serve multiple households simultaneously—this is called multi-threading, and you are the main thread.
- The tools used by the workers are provided by the property management company, which are shared among everyone—not exclusive to any one person—this is called resource sharing in multi-threading.
- Workers need wrenches for their work, but there’s only one wrench—this is called conflict. Solutions include queuing, waiting for colleagues to finish using it via WeChat notifications—this is called thread synchronization.
- You assign tasks to the workers—this is called creating threads. After assigning tasks, you must tell them they can start working; otherwise, they will remain idle—this is called starting a thread (start).
- If a worker (thread) has an important task, you (main thread) might personally supervise for a period of time; if not specified, it means you’ll keep supervising until the task is completed—this is called thread participation (join).
- When business is light, you just sit in the office drinking tea. When the work hours end, you send a WeChat group message to notify workers to leave. All workers stop immediately, regardless of whether they finish their tasks—this is called setting and managing daemon threads.
- Later, as the company grows, it serves many communities. Each community has its own branch office, managed by branch managers, with almost identical operations mechanisms as the headquarters—this is called multi-processes, where the headquarters is the main process and the branches are child processes.
- The headquarters and branches, as well as between branches, use independent tools that cannot be borrowed or mixed—this is called processes not sharing resources. Branches can communicate via dedicated phone lines—this is called pipes. Branches can also exchange information via company bulletin boards—this is called shared memory between processes. Additionally, branches have various coordination methods to complete larger-scale tasks—this is called process synchronization.
- Branches can leave with the headquarters or wait until all work is done before leaving—this is called setting daemon processes.
