Why Python Threads Can’t Fully Utilize Multi‑Core CPUs and How to Use Them Effectively
This tutorial explains Python's multithreading model, its advantages and limitations—including the Global Interpreter Lock—demonstrates thread creation with the threading module, shows race conditions and lock usage, compares processes and threads, and discusses when to choose threads, processes, or asynchronous I/O for different workloads.
Multithreading in Python
Python supports true OS‑level threads (Posix threads) via its threading module, but due to the Global Interpreter Lock (GIL) only one thread executes Python bytecode at a time.
Advantages of multithreading
Long‑running tasks can be moved to background threads.
User interfaces stay responsive; progress can be shown.
Potential speed‑up when tasks spend time waiting (I/O, user input, etc.).
Resources such as memory can be shared among threads.
Thread types
Kernel threads : created and destroyed by the operating system.
User threads : implemented in user space without kernel support.
Python threading modules
The low‑level _thread module provides basic primitives, while the high‑level threading module wraps it and is recommended for most use cases.
Example: Download files with threads
from random import randint
from threading import Thread, current_thread
from time import time, sleep
def download(filename):
print('thread %s is running...' % current_thread().name)
print('开始下载%s...' % filename)
time_to_download = randint(5, 10)
sleep(time_to_download)
print('%s下载完成! 耗费了%d秒' % (filename, time_to_download))
def download_multi_threading():
print('thread %s is running...' % current_thread().name)
start = time()
t1 = Thread(target=download, args=('Python.pdf',), name='subthread-1')
t1.start()
t2 = Thread(target=download, args=('nazha.mkv',), name='subthread-2')
t2.start()
t1.join()
t2.join()
end = time()
print('总共耗费了%.3f秒' % (end - start))
print('thread %s is running...' % current_thread().name)
if __name__ == '__main__':
download_multi_threading()Threads are created with Thread, specifying the target function, arguments, and an optional name. The main thread can start, join, and monitor child threads.
Lock and race conditions
When multiple threads modify a shared variable, race conditions can corrupt data because each operation consists of several steps that may be interleaved.
from threading import Thread
from time import sleep
balance = 0
def change_it(n):
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
def nolock_multi_thread():
t1 = Thread(target=run_thread, args=(5,))
t2 = Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
if __name__ == '__main__':
nolock_multi_thread()Running the code often prints a non‑zero balance (e.g., -8) because the two threads interleave the read‑modify‑write steps.
Using a lock to protect shared data
from threading import Thread, Lock
from time import sleep
balance = 0
lock = Lock()
def change_it(n):
global balance
balance = balance + n
balance = balance - n
def run_thread_lock(n):
for i in range(100000):
lock.acquire()
try:
change_it(n)
finally:
lock.release()
def lock_multi_thread():
t1 = Thread(target=run_thread_lock, args=(5,))
t2 = Thread(target=run_thread_lock, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
if __name__ == '__main__':
lock_multi_thread()The lock ensures that only one thread modifies balance at a time, guaranteeing the final result is zero.
GIL limitation
CPython’s Global Interpreter Lock forces a thread to acquire the GIL before executing bytecode and releases it after about 100 bytecode instructions, so CPU‑bound Python threads cannot run truly in parallel; they merely interleave on a single core.
Process vs Thread
Processes provide higher stability because each has its own memory space; a crash in one does not affect others. Threads share memory, offering faster context switches but lower fault isolation. Creating processes is heavier, especially on Windows, while threads are lighter.
Master‑Worker model
Typical concurrent designs use a master (or manager) to distribute tasks to multiple workers, which can be implemented with either processes or threads.
Compute‑bound vs I/O‑bound tasks
Compute‑intensive workloads (e.g., video encoding) benefit from multiple processes to bypass the GIL. I/O‑intensive workloads (network, disk) can achieve good performance with multithreading because threads spend most of their time waiting.
Asynchronous I/O and coroutines
Modern OSes support asynchronous I/O, allowing a single‑threaded event‑driven model (coroutines) to handle many I/O tasks efficiently. In Python, async/await provides this model, eliminating the need for thread‑level locks.
References
https://www.liaoxuefeng.com/wiki/1016959663602400/1017627212385376
https://github.com/jackfrued/Python-100-Days/blob/master/Day01-15/13.%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B.md
https://www.runoob.com/python3/python3-multithreading.html
The full source code is available at GitHub .
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Python Crawling & Data Mining
Life's short, I code in Python. This channel shares Python web crawling, data mining, analysis, processing, visualization, automated testing, DevOps, big data, AI, cloud computing, machine learning tools, resources, news, technical articles, tutorial videos and learning materials. Join us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
