Python3 Multithreading
Multithreading is similar to running multiple different programs simultaneously. Multithreading has the following advantages:
- Using threads can put tasks from long-running programs into the background for processing.
- User interfaces can be more attractive, for example, when a user clicks a button to trigger some event processing, a progress bar can pop up to show the processing progress.
- The program’s running speed may be accelerated.
- In some waiting tasks such as user input, file reading/writing, and network data sending/receiving, threads are more useful. In such cases, we can release some precious resources such as memory usage, etc.
Each independent thread has a program entry point, a sequential execution sequence, and a program exit. However, threads cannot execute independently; they must exist within an application, which provides multiple threads with execution control.
Each thread has its own set of CPU registers, called the thread’s context, which reflects the state of the CPU registers the last time the thread ran.
The instruction pointer and stack pointer registers are the two most important registers in the thread context. Threads always run within the context of a process, and these addresses are used to mark memory within the process’s address space that owns the thread.
- Threads can be preempted (interrupted).
- While other threads are running, a thread can be temporarily suspended (also called sleeping) – this is thread yielding.
Threads can be divided into:
- Kernel threads: Created and destroyed by the operating system kernel.
- User threads: Threads implemented in user programs without kernel support.
Two commonly used modules for threads in Python3 are:
- _thread
- threading (recommended)
The thread module has been deprecated. Users can use the threading module instead. Therefore, in Python3, you can no longer use the “thread” module. For compatibility, Python3 renamed thread to “_thread”.
Getting Started with Python Threads
Section titled “Getting Started with Python Threads”There are two ways to use threads in Python: using functions or wrapping thread objects with classes.
Functional: Call the start_new_thread() function in the _thread module to spawn a new thread. The syntax is as follows:
Parameter description:
- function - The thread function.
- args - Arguments passed to the thread function; it must be a tuple type.
- kwargs - Optional parameters.
Example
Section titled “Example”The output of the above program is:
After executing the above program, you can press ctrl-c to exit.
Threading Module
Section titled “Threading Module”Python3 provides support for threads through two standard libraries: _thread and threading.
_thread provides low-level, primitive threads and a simple lock. Compared to the threading module, its functionality is relatively limited.
In addition to containing all methods in the _thread module, the threading module also provides other methods:
- threading.current_thread(): Returns the current thread variable.
- threading.enumerate(): Returns a list containing all running threads. “Running” means after the thread has started and before it has finished, excluding threads before start and after termination.
- threading.active_count(): Returns the number of running threads, which has the same result as len(threading.enumerate()).
- threading.Thread(target, args=(), kwargs={}, daemon=None):
- Creates an instance of the
Threadclass. target: The target function to be executed by the thread.args: Arguments to the target function, passed as a tuple.kwargs: Keyword arguments to the target function, passed as a dictionary.daemon: Specifies whether the thread is a daemon thread.
- Creates an instance of the
The threading.Thread class provides the following methods and properties:
-
__init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):- Initializes the
Threadobject. group: Thread group, currently unused, reserved for future extension.target: The target function to be executed by the thread.name: The name of the thread.args: Arguments to the target function, passed as a tuple.kwargs: Keyword arguments to the target function, passed as a dictionary.daemon: Specifies whether the thread is a daemon thread.
- Initializes the
-
start(self):- Starts the thread. Will call the thread’s
run()method.
- Starts the thread. Will call the thread’s
-
run(self):- The thread defines the code to be executed in this method.
-
join(self, timeout=None):- Waits for the thread to terminate. By default,
join()blocks until the called thread terminates. If thetimeoutparameter is specified, it waits at mosttimeoutseconds.
- Waits for the thread to terminate. By default,
-
is_alive(self):- Returns whether the thread is running. Returns
Trueif the thread has started and not yet terminated, otherwise returnsFalse.
- Returns whether the thread is running. Returns
-
getName(self):- Returns the name of the thread.
-
setName(self, name):- Sets the name of the thread.
-
identproperty:- The unique identifier of the thread.
-
daemonproperty:- The daemon flag of the thread, used to indicate whether it is a daemon thread.
-
isDaemon()method:
A simple thread example:
Example
Section titled “Example”Output:
Creating Threads Using the threading Module
Section titled “Creating Threads Using the threading Module”We can create a new subclass by directly inheriting from threading.Thread, instantiate it, and call the start() method to start the new thread, which calls the thread’s run() method:
Example
Section titled “Example”The output of the above program is:
Thread Synchronization
Section titled “Thread Synchronization”If multiple threads modify the same data, unpredictable results may occur. To ensure data correctness, multiple threads need to be synchronized.
Using the Lock and Rlock of the Thread object can achieve simple thread synchronization. Both objects have acquire and release methods. For data that needs to be operated on by only one thread at a time, put its operations between the acquire and release methods. As follows:
The advantage of multithreading is that multiple tasks can run simultaneously (or at least feel that way). However, when threads need to share data, data synchronization issues may arise.
Consider this scenario: all elements in a list are 0. The “set” thread changes all elements from back to front to 1, while the “print” thread reads the list from front to back and prints it.
Then, it’s possible that when the “set” thread starts modifying, the “print” thread starts printing the list. The output would be half 0s and half 1s. This is data desynchronization. To avoid this situation, the concept of locks was introduced.
A lock has two states – locked and unlocked. Whenever a thread, such as “set”, wants to access shared data, it must first acquire the lock. If another thread, such as “print”, has already acquired the lock, then the “set” thread is suspended, which is synchronous blocking. Once the “print” thread finishes accessing and releases the lock, the “set” thread can continue.
After this processing, when printing the list, it will output either all 0s or all 1s, avoiding the awkward situation of half 0s and half 1s.
Example
Section titled “Example”The output of the above program is:
Thread Priority Queue (Queue)
Section titled “Thread Priority Queue (Queue)”Python’s Queue module provides synchronized, thread-safe queue classes, including FIFO (First In First Out) queue Queue, LIFO (Last In First Out) queue LifoQueue, and priority queue PriorityQueue.
These queues implement lock primitives and can be used directly in multithreading. Queues can be used to implement synchronization between threads.
Common methods in the Queue module:
- Queue.qsize() Returns the size of the queue
- Queue.empty() If the queue is empty, returns True, otherwise False
- Queue.full() If the queue is full, returns True, otherwise False
- Queue.full Corresponds to the maxsize size
- Queue.get([block[, timeout]]) Gets from the queue, timeout wait time
- Queue.get_nowait() Equivalent to Queue.get(False)
- Queue.put(item) Writes to the queue, timeout wait time
- Queue.put_nowait(item) Equivalent to Queue.put(item, False)
- Queue.task_done() After completing a task, Queue.task_done() sends a signal to the queue that the task has been completed
- Queue.join() Actually means to wait until the queue is empty before performing other operations
Example
Section titled “Example”The output of the above program is: