Python 多线程与并发:概念与实践入门
在现代计算机应用中,我们常常需要程序能够同时处理多项任务,或者在等待某个操作(如网络请求、文件读写)完成时,不阻塞整个程序的运行。这就是并发(Concurrency)的概念。而多线程(Multithreading)是实现并发的一种常见且重要的方式。
Python 语言提供了对多线程的良好支持,通过标准库中的 threading
模块,我们可以相对便捷地编写并发程序。然而,Python 的多线程有着其特殊性——全局解释器锁(GIL),理解 GIL 对于正确使用 Python 多线程至关重要。
本文将详细介绍 Python 中多线程和并发的基本概念、threading
模块的使用方法、线程同步机制、GIL 的影响,以及何时应该使用多线程。
1. 理解并发与并行
在深入多线程之前,先区分两个容易混淆的概念:并发(Concurrency)与并行(Parallelism)。
- 并发 (Concurrency): 指的是系统能够同时处理多个任务,但这些任务不一定在同一时刻真正地运行。在单核 CPU 上,并发是通过 CPU 时间片的快速切换来实现的,给人一种“同时进行”的错觉。多个任务在单个 CPU 核心上轮流执行,共享计算资源。
- 并行 (Parallelism): 指的是系统能够让多个任务在同一时刻真正地运行。这通常需要多核 CPU 或多处理器系统,每个核心或处理器独立地执行一个任务。任务是真正地“同时”执行。
简单来说,并发是让多个任务看起来在同时进行,而并行是让多个任务真正地同时进行。
多线程是实现并发的一种方式。在 Python 中,由于 GIL 的存在(我们后面会详细解释),标准的多线程在 CPU 密集型任务上通常无法实现真正的并行,但在 I/O 密集型任务上可以实现并发,显著提高效率。
2. 进程与线程
理解进程和线程是理解多任务编程的基础。
- 进程 (Process): 是操作系统进行资源分配和调度的基本单位。一个程序运行起来就是一个进程。每个进程都有自己独立的内存空间、数据栈以及其他资源(文件句柄、网络连接等)。进程之间的数据共享比较复杂,通常需要特定的IPC(Inter-Process Communication,进程间通信)机制。创建进程的开销比较大。
- 线程 (Thread): 是进程内部的一个执行流,是 CPU 调度的基本单位。一个进程可以包含一个或多个线程。同一进程内的所有线程共享该进程的内存空间、文件句柄等资源。线程之间的数据共享比较容易(直接访问共享变量),但也更容易引发同步问题。创建线程的开销相对较小。
可以把进程想象成一个工厂,线程就是工厂里的工人。同一个工厂(进程)里的工人们(线程)共享厂房(内存空间),但每个工人有自己的工作台和工具(独立的栈和寄存器状态)。工人们可以很容易地交流和共享原材料(共享数据),但也需要协调工作,避免冲突(同步)。不同的工厂(进程)之间则需要通过专门的通道(IPC)来交换信息。
在 Python 中,multiprocessing
模块用于创建和管理进程,而 threading
模块用于创建和管理线程。本文聚焦于 threading
模块。
3. Python threading
模块入门
Python 的标准库 threading
模块提供了创建和管理线程的功能。
3.1 创建线程
创建线程有两种主要方法:
- 创建一个
Thread
对象,并将要执行的函数作为参数传递。 - 创建一个继承自
Thread
类的子类,并重写run()
方法。
方法一:使用 Thread
对象
这是更常用、更简洁的方式。
“`python
import threading
import time
def worker(num):
“””线程执行的函数”””
print(f”Worker {num} starting”)
time.sleep(1) # 模拟耗时操作
print(f”Worker {num} finishing”)
创建线程对象
thread1 = threading.Thread(target=worker, args=(1,))
thread2 = threading.Thread(target=worker, args=(2,))
print(“Main thread: creating threads”)
启动线程
thread1.start()
thread2.start()
print(“Main thread: threads started”)
主线程等待所有子线程完成
thread1.join()
thread2.join()
print(“Main thread: all threads finished”)
“`
解释:
threading.Thread(target=function, args=(arg1, arg2, ...))
:创建一个Thread
对象。target
: 指定线程启动时要执行的函数。args
: 以元组形式向目标函数传递参数。如果只有一个参数,也要写成(arg1,)
形式,最后一个逗号不能省略。
thread.start()
: 启动线程,调用目标函数,使其开始执行。thread.join()
: 阻塞主线程(或调用join()
的线程),直到被join()
的那个线程执行完毕。这确保了在主线程退出之前,所有重要的子线程都已经完成了工作。
方法二:继承 Thread
类
这种方法适用于需要创建更复杂的线程对象,或者需要在线程对象内部维护状态的情况。
“`python
import threading
import time
class MyThread(threading.Thread):
def init(self, num):
super().init() # 调用父类构造器
self.num = num
def run(self):
"""线程执行的方法"""
print(f"MyThread {self.num} starting")
time.sleep(1.5)
print(f"MyThread {self.num} finishing")
创建线程实例
thread3 = MyThread(3)
thread4 = MyThread(4)
print(“Main thread: creating MyThread instances”)
启动线程(实际上是调用 run 方法)
thread3.start()
thread4.start()
print(“Main thread: MyThread instances started”)
主线程等待子线程完成
thread3.join()
thread4.join()
print(“Main thread: all MyThread instances finished”)
“`
解释:
- 创建一个类
MyThread
继承自threading.Thread
。 - 重写
run()
方法。当调用线程对象的start()
方法时,实际上就是执行这个run()
方法中的代码。 - 在
__init__
方法中,务必调用super().__init__()
来初始化父类Thread
。
两种方法都可以实现线程的创建和执行,根据具体需求选择合适的方式。对于简单的并发任务,方法一更简洁;对于需要封装线程逻辑和状态的场景,方法二更合适。
3.2 线程的生命周期
一个线程从创建到结束,会经历以下几个阶段:
- 新建 (New): 线程对象已经被创建,但还没有调用
start()
方法。 - 可运行 (Runnable): 调用了
start()
方法后,线程进入可运行状态。它现在有资格获得 CPU 时间片,等待操作系统的调度。 - 运行中 (Running): 线程正在获得 CPU 时间片,其
run()
方法正在执行。 - 阻塞 (Blocked): 线程因为某种原因暂停执行,例如调用了
time.sleep()
、等待 I/O 操作完成、等待获取锁等。当阻塞的原因解除后,线程会重新回到可运行状态。 - 死亡 (Dead): 线程的
run()
方法执行完毕,或者因为异常而退出。线程对象仍然存在,但线程的执行已经停止,不能再次启动。
3.3 守护线程 (Daemon Threads)
线程可以被设置为守护线程。守护线程的特点是,当主线程退出时,即使守护线程还没有执行完毕,它们也会被强制终止。非守护线程(默认)则会阻止主线程退出,直到它们全部执行完毕。
“`python
import threading
import time
def daemon_worker():
print(“Daemon worker starting (will run for 3 seconds)…”)
time.sleep(3)
print(“Daemon worker finishing (maybe not visible if main thread exits early)”)
d_thread = threading.Thread(target=daemon_worker, daemon=True) # 设置为守护线程
d_thread.start()
print(“Main thread exiting after 1 second.”)
time.sleep(1) # 主线程只运行1秒
注意:这里没有调用 d_thread.join()
如果是守护线程,主线程退出时它会被强制终止
如果是非守护线程(默认),主线程会等待它运行3秒完成后才退出
“`
将 daemon
参数设置为 True
来创建守护线程。守护线程常用于执行一些后台任务,如日志记录、垃圾回收等,它们的存在不应阻止程序的主体部分退出。
4. 线程同步:避免竞态条件
由于同一进程内的线程共享内存空间,当多个线程同时访问和修改同一个共享资源时,如果没有适当的控制,可能会导致数据不一致或错误的结果。这种现象称为竞态条件 (Race Condition)。
考虑一个简单的例子:多个线程对一个全局计数器进行递增操作。
“`python
import threading
counter = 0
lock = threading.Lock() # 暂不使用锁
def increment():
global counter
# lock.acquire() # 尝试获取锁
# try:
local_copy = counter
local_copy += 1
time.sleep(0.01) # 模拟一些工作,增加竞态条件发生的几率
counter = local_copy
# finally:
# lock.release() # 释放锁
threads = []
for _ in range(5): # 创建5个线程
thread = threading.Thread(target=increment)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f”Final counter value: {counter}”) # 理论上应该是 5
“`
运行上面的代码多次,你会发现最终 counter
的值很可能不是 5,而是一个小于 5 的数。这是因为多个线程同时读取 counter
的当前值,各自在其局部副本上进行递增,然后将局部副本写回全局 counter
。写回的操作可能会覆盖其他线程的修改,导致部分递增操作“丢失”。
为了解决竞态条件,我们需要使用线程同步(Thread Synchronization)机制来控制对共享资源的访问。Python 的 threading
模块提供了多种同步原语(Synchronization Primitives)。
4.1 锁 (Lock)
锁是最基本的同步原语。一个锁有两种状态:锁定 (locked) 和 未锁定 (unlocked)。一个线程可以通过 acquire()
方法尝试获取锁。如果锁是未锁定的,线程会成功获取锁并将其状态设置为锁定;如果锁是锁定的,线程会阻塞,直到持有锁的线程通过 release()
方法释放锁。
修改上面的例子,使用 Lock
来保证原子性操作:
“`python
import threading
import time
counter = 0
lock = threading.Lock() # 创建一个锁对象
def increment_with_lock():
global counter
lock.acquire() # 尝试获取锁,如果获取不到则阻塞
try:
# 这段代码(临界区)在同一时间只能被一个线程执行
local_copy = counter
local_copy += 1
time.sleep(0.01)
counter = local_copy
finally:
lock.release() # 释放锁
threads = []
for _ in range(5):
thread = threading.Thread(target=increment_with_lock)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(f”Final counter value with lock: {counter}”) # 现在应该总是 5
“`
使用 with
语句是更推荐的获取和释放锁的方式,它可以确保锁在代码块执行完毕(无论是否发生异常)后自动释放,避免死锁。
“`python
def increment_with_lock_and_with():
global counter
with lock: # 等价于 lock.acquire(),并在块结束后自动 lock.release()
local_copy = counter
local_copy += 1
time.sleep(0.01)
counter = local_copy
… rest of the code is similar …
“`
4.2 可重入锁 (RLock)
RLock
(Recursive Lock) 是另一种锁。与 Lock
不同,同一个线程可以多次获取 RLock
,但必须释放相同次数的 RLock
才能让其他线程获取。这在递归函数或同一线程需要多次加锁的场景下非常有用。如果使用 Lock
,同一个线程多次 acquire()
会导致死锁。
“`python
import threading
lock = threading.Lock() # 会导致死锁
rlock = threading.RLock() # 使用 RLock
def recursive_function(level):
with rlock:
print(f”Thread {threading.current_thread().name} acquired lock at level {level}”)
if level > 0:
recursive_function(level – 1)
print(f”Thread {threading.current_thread().name} released lock at level {level}”)
thread = threading.Thread(target=lambda: recursive_function(2))
thread.start()
thread.join()
“`
在这个例子中,一个线程多次获取同一个锁,如果使用 Lock
会死锁,使用 RLock
则正常运行。
4.3 信号量 (Semaphore)
信号量是一种更高级的同步原语,用于控制对有限资源的访问。它维护一个内部计数器,计数器初始值为资源的可用数量。每次 acquire()
信号量时,计数器减一;每次 release()
信号量时,计数器加一。当计数器为零时,后续的 acquire()
操作会阻塞,直到有其他线程 release()
信号量使计数器大于零。
例如,限制同时访问某个资源的线程数量:
“`python
import threading
import time
import random
允许最多 3 个线程同时访问
semaphore = threading.Semaphore(3)
def access_resource(thread_id):
print(f”Thread {thread_id} attempting to access resource…”)
with semaphore: # acquire the semaphore
print(f”Thread {thread_id} accessed resource.”)
sleep_time = random.randint(1, 3)
time.sleep(sleep_time) # 模拟使用资源
print(f”Thread {thread_id} releasing resource after {sleep_time} seconds.”)
# semaphore is automatically released here
threads = []
for i in range(10): # 10个线程竞争3个资源
thread = threading.Thread(target=access_resource, args=(i,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print(“All threads finished.”)
“`
运行代码,你会看到“accessed resource”的打印信息最多同时出现 3 个。
4.4 事件 (Event)
事件是一种简单的线程通信机制。一个事件对象维护一个内部标志,可以设置为“真”或“假”。线程可以通过 wait()
方法等待事件标志变为真,如果标志为假,wait()
会阻塞;如果标志为真,wait()
立即返回。另一个线程可以通过 set()
方法将标志设置为真,解除所有等待在该事件上的线程的阻塞;通过 clear()
方法将标志设置为假。
“`python
import threading
import time
创建一个事件对象
event = threading.Event()
def waiter(num):
print(f”Waiter {num} is waiting for event…”)
event.wait() # 阻塞直到事件标志变为真
print(f”Waiter {num} received event! Proceeding.”)
def setter():
print(“Setter is sleeping for 2 seconds before setting event…”)
time.sleep(2)
print(“Setter is setting event.”)
event.set() # 将事件标志设置为真
创建并启动等待者线程
waiter_threads = []
for i in range(3):
thread = threading.Thread(target=waiter, args=(i,))
waiter_threads.append(thread)
thread.start()
创建并启动设置者线程
setter_thread = threading.Thread(target=setter)
setter_thread.start()
等待所有线程完成
for thread in waiter_threads:
thread.join()
setter_thread.join()
print(“All threads finished.”)
“`
这个例子中,所有 waiter
线程会一直阻塞,直到 setter
线程调用 event.set()
。
4.5 条件变量 (Condition)
条件变量比事件更灵活,它通常与锁一起使用,允许线程在某个条件不满足时释放锁并进入等待状态,直到另一个线程满足条件并发出通知。当一个线程需要访问共享资源但某个条件尚未满足时,它会释放持有的锁(如果有)并调用 wait()
。当另一个线程改变了共享资源的状态并使条件可能满足时,它可以调用 notify()
或 notify_all()
通知等待在该条件变量上的线程。被通知的线程会重新尝试获取锁,并在成功获取锁后从 wait()
调用处继续执行。
“`python
import threading
import time
import random
创建一个条件变量,它内部会包含一个锁
condition = threading.Condition()
items = [] # 共享资源:一个列表
def producer():
while True:
with condition: # 获取条件变量内部的锁
if len(items) < 5:
item = random.randint(1, 100)
items.append(item)
print(f”Producer produced item {item}. Current items: {items}”)
condition.notify() # 通知等待者(消费者)有新物品了
else:
print(“Producer: items list is full, waiting for consumer…”)
condition.wait() # 释放锁并等待通知
time.sleep(random.uniform(0.5, 1.5)) # 模拟生产间隔
def consumer():
while True:
with condition: # 获取条件变量内部的锁
if not items:
print(“Consumer: items list is empty, waiting for producer…”)
condition.wait() # 释放锁并等待通知
else:
item = items.pop(0)
print(f”Consumer consumed item {item}. Remaining items: {items}”)
# condition.notify() # 也可以通知生产者,但在这个例子中不是必需的
time.sleep(random.uniform(0.5, 1.5)) # 模拟消费间隔
创建并启动生产者和消费者线程
producer_thread = threading.Thread(target=producer, daemon=True) # 守护线程,主程序退出时终止
consumer_thread = threading.Thread(target=consumer, daemon=True) # 守护线程
producer_thread.start()
consumer_thread.start()
主线程运行一段时间后退出
print(“Main thread running…”)
time.sleep(10)
print(“Main thread exiting.”) # 守护线程会自动终止
“`
这是一个经典的生产者-消费者问题示例。生产者在列表满时等待消费者消费,消费者在列表空时等待生产者生产。Condition
提供了一种优雅的方式来实现这种等待和通知机制。
4.6 队列 (Queue)
在多线程编程中,最推荐的数据共享和线程间通信方式是使用 queue.Queue
。Queue
是线程安全的,内部使用了锁和条件变量等同步机制。生产者可以将数据放入队列,消费者可以从队列中取出数据,而无需手动管理锁,避免了复杂的同步问题。
“`python
import threading
import queue
import time
import random
创建一个线程安全的队列
data_queue = queue.Queue()
def producer_queue(q):
for i in range(5):
item = random.randint(1, 100)
print(f”Producer: Putting {item} into queue.”)
q.put(item) # 将数据放入队列,如果队列满则阻塞
time.sleep(random.uniform(0.5, 1))
q.put(None) # 发送结束信号
def consumer_queue(q):
while True:
print(“Consumer: Attempting to get item from queue…”)
item = q.get() # 从队列取出数据,如果队列空则阻塞
if item is None: # 收到结束信号
print(“Consumer: Received termination signal.”)
q.task_done() # 通知队列该任务已处理完毕
break
print(f”Consumer: Got {item} from queue.”)
time.sleep(random.uniform(0.5, 1))
q.task_done() # 通知队列该任务已处理完毕
创建并启动线程
producer_thread = threading.Thread(target=producer_queue, args=(data_queue,))
consumer_thread = threading.Thread(target=consumer_queue, args=(data_queue,))
producer_thread.start()
consumer_thread.start()
等待队列中的所有任务都完成(包括get和task_done调用次数相等)
data_queue.join() # 阻塞主线程直到队列中的所有任务都标记为完成
print(“Main thread: All queue tasks done. Exiting.”)
“`
使用 queue.Queue
极大地简化了线程间的数据传递和同步问题,是 Python 多线程编程中非常实用的工具。q.put()
和 q.get()
方法默认是阻塞的,也可以通过设置 block=False
或 timeout
参数来实现非阻塞或带超时的操作。q.task_done()
和 q.join()
配合使用,可以方便地判断队列中的所有项目是否已被处理。
5. 全局解释器锁 (GIL)
现在来谈谈 Python 多线程中最具争议和最需要理解的部分:全局解释器锁 (GIL)。
GIL 是 CPython(Python 解释器最常用的一种实现)中的一个机制,它是一个互斥锁,限制了在任何时候只有一个线程可以执行 Python 字节码。
为什么会有 GIL?
GIL 的主要目的是为了简化 CPython 内存管理的实现,特别是垃圾回收。CPython 使用引用计数来管理内存,当一个对象的引用计数变为零时,它就被回收。如果在多线程环境下没有 GIL,多个线程同时操作同一个对象的引用计数,可能导致计数错误,从而引发内存泄漏或程序崩溃。GIL 保证了在操作引用计数等关键数据结构时,只有一个线程在执行,从而避免了这些问题。
GIL 对多线程的影响
GIL 对多线程的影响是:
- 对于 CPU 密集型任务: 例如大量的计算、循环、矩阵运算等,这些任务会一直占用 CPU。由于 GIL 的存在,即使在多核 CPU 上创建多个线程,也只有一个线程能够真正地在某个时刻执行 Python 字节码。其他线程会被阻塞,直到当前线程释放 GIL。这导致 Python 多线程在 CPU 密集型任务上无法利用多核优势实现真正的并行,甚至可能因为线程切换的开销而比单线程更慢。
- 对于 I/O 密集型任务: 例如文件读写、网络请求、数据库交互等,这些任务的特点是线程在大部分时间都在等待外部资源(磁盘、网络)的响应,而不是占用 CPU。在进行这些阻塞的 I/O 操作时,CPython 解释器会主动释放 GIL,允许其他线程运行。当 I/O 操作完成后,线程会尝试重新获取 GIL。因此,在 I/O 密集型任务中,多个线程可以并发地执行(虽然不是真正的并行),一个线程等待 I/O 时,其他线程可以执行,从而提高整体效率。
总结 GIL 的影响:
Python 的 threading
模块适合用于处理 I/O 密集型任务,因为它可以在等待 I/O 时切换到其他线程,提高并发性。它不适合用于处理 CPU 密集型任务,因为 GIL 限制了它在多核 CPU 上的并行能力。
如果你需要利用多核处理 CPU 密集型任务,应该使用 Python 的 multiprocessing
模块,它通过创建多个进程来规避 GIL,因为每个进程都有自己的 Python 解释器和独立的 GIL。
6. 何时使用多线程
基于对并发、进程、线程以及 GIL 的理解,我们可以总结何时应该使用 Python 多线程:
- I/O 密集型任务: 这是 Python 多线程最主要的用武之地。例如:
- 同时下载多个文件
- 同时发起多个网络请求(爬虫、API 调用)
- 同时读写多个文件
- 与数据库进行交互(等待数据库响应)
- 处理用户输入(等待用户输入)
在这些场景下,线程在等待外部资源时释放 GIL,其他线程可以继续工作,从而提高程序的响应速度和吞吐量。
- 需要并行执行但又需要共享内存的应用: 如果任务确实需要在同一个地址空间内操作共享数据(例如,修改同一个大型数据结构),并且这些任务是 I/O 密集型的,那么多线程是一个选择。然而,需要非常小心地处理同步问题。
- 简化程序结构: 有时使用多线程只是为了让程序逻辑更清晰,例如将用户界面、后台数据处理、网络通信等不同职责分配给不同的线程。
何时不使用或慎用多线程:
- CPU 密集型任务: 如果你的任务主要是进行大量计算,如图像处理、数值计算、数据分析等,Python 多线程无法利用多核优势,应考虑使用
multiprocessing
模块、其他编程语言(如 C, C++)或使用 NumPy 等底层库优化过的计算。 - 任务之间高度耦合,频繁共享和修改数据: 尽管线程共享内存很方便,但这也很容易导致复杂的同步问题和死锁。如果任务之间需要大量且复杂的同步,可能会使得多线程代码难以理解和维护,有时单线程或更高级的并发模型(如异步编程
asyncio
)可能更合适。
7. 线程池 (ThreadPoolExecutor)
直接创建和管理大量线程可能会带来一些开销,并且难以控制并发的数量。Python 3.2+ 引入的 concurrent.futures
模块提供了一个更高级的接口来管理并发任务,其中包括线程池 ThreadPoolExecutor
。线程池维护一个固定数量的线程,可以将任务提交给线程池,由线程池中的线程来执行。这减少了线程创建/销毁的开销,并限制了同时运行的线程数量。
“`python
import concurrent.futures
import time
import random
def task(name):
print(f”Task {name}: starting”)
sleep_time = random.randint(1, 3)
time.sleep(sleep_time) # Simulate I/O or work
print(f”Task {name}: finished after {sleep_time} seconds”)
return f”Task {name} result”
创建一个最大工作线程数为 3 的线程池
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
# 提交任务给线程池
future1 = executor.submit(task, “A”)
future2 = executor.submit(task, “B”)
future3 = executor.submit(task, “C”)
future4 = executor.submit(task, “D”) # 这个任务会等待有线程空闲
# 获取任务结果 (阻塞直到任务完成)
print(f"Result of Task A: {future1.result()}")
print(f"Result of Task B: {future2.result()}")
# 也可以通过 as_completed 迭代已完成的任务
print("\nIterating over completed tasks:")
futures = [executor.submit(task, str(i)) for i in range(5, 10)]
for future in concurrent.futures.as_completed(futures):
print(f"Completed: {future.result()}")
print(“All tasks submitted via ThreadPoolExecutor have completed.”)
“`
ThreadPoolExecutor
简化了线程的管理,特别是对于需要执行大量相似且独立的并发任务的情况。submit()
方法提交任务并返回一个 Future
对象,可以通过 future.result()
获取任务的返回值。as_completed()
提供了一个方便的方式来按任务完成顺序处理结果。
8. 总结与最佳实践
本文介绍了 Python 多线程和并发的基础知识,包括:
- 并发与并行的区别。
- 进程与线程的概念。
- 使用
threading
模块创建和启动线程,包括Thread
对象和继承Thread
类。 start()
和join()
方法的使用。- 守护线程的概念。
- 竞态条件及其危害。
- 使用同步原语(
Lock
,RLock
,Semaphore
,Event
,Condition
)解决线程同步问题。 - 使用
queue.Queue
进行线程间安全通信,这是推荐的方式。 - 理解全局解释器锁 (GIL) 对 Python 多线程性能的影响:适合 I/O 密集型任务,不适合 CPU 密集型任务。
- 何时应该使用多线程以及替代方案(
multiprocessing
,asyncio
)。 - 使用
concurrent.futures.ThreadPoolExecutor
更方便地管理线程池。
编写 Python 多线程代码的最佳实践:
- 明确任务类型: 在决定使用多线程之前,先分析你的任务是 I/O 密集型还是 CPU 密集型。I/O 密集型是多线程的理想场景。
- 最小化共享状态: 尽量减少线程之间共享的数据。共享数据越多,需要处理的同步问题就越复杂。
- 优先使用
queue.Queue
进行线程间通信:Queue
是线程安全的,可以有效避免手动加锁带来的复杂性和潜在错误。 - 使用
with
语句管理锁:with lock:
结构可以确保锁在代码块执行完毕后自动释放,即使发生异常,有效防止死锁。 - 理解并小心使用同步原语: 锁、信号量等是强大的工具,但也容易引入死锁或其他并发问题。仔细思考和设计你的同步逻辑。
- 合理使用
join()
或线程池: 确保在主线程退出前,重要的子线程已经完成工作。使用线程池可以更方便地管理一组并发任务。 - 避免长时间持有 GIL 的操作: 如果可能,将 CPU 密集型计算部分剥离到其他进程、使用 C 扩展或利用 NumPy 等库。
- 测试并发代码: 并发问题往往难以重现,需要仔细设计测试用例,并多次运行以发现潜在的竞态条件或死锁。
Python 的多线程是实现并发的一种强大工具,尤其在处理 I/O 密集型任务时能显著提升效率。掌握其基本概念、同步机制以及 GIL 的影响,是编写健壮、高效并发程序的关键一步。随着实践的深入,你会对何时以及如何更有效地利用 Python 的并发能力有更深刻的理解。