Python 多线程和并发:概念与实践入门 – wiki基地


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 创建线程

创建线程有两种主要方法:

  1. 创建一个 Thread 对象,并将要执行的函数作为参数传递。
  2. 创建一个继承自 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.QueueQueue 是线程安全的,内部使用了锁和条件变量等同步机制。生产者可以将数据放入队列,消费者可以从队列中取出数据,而无需手动管理锁,避免了复杂的同步问题。

“`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=Falsetimeout 参数来实现非阻塞或带超时的操作。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 多线程代码的最佳实践:

  1. 明确任务类型: 在决定使用多线程之前,先分析你的任务是 I/O 密集型还是 CPU 密集型。I/O 密集型是多线程的理想场景。
  2. 最小化共享状态: 尽量减少线程之间共享的数据。共享数据越多,需要处理的同步问题就越复杂。
  3. 优先使用 queue.Queue 进行线程间通信: Queue 是线程安全的,可以有效避免手动加锁带来的复杂性和潜在错误。
  4. 使用 with 语句管理锁: with lock: 结构可以确保锁在代码块执行完毕后自动释放,即使发生异常,有效防止死锁。
  5. 理解并小心使用同步原语: 锁、信号量等是强大的工具,但也容易引入死锁或其他并发问题。仔细思考和设计你的同步逻辑。
  6. 合理使用 join() 或线程池: 确保在主线程退出前,重要的子线程已经完成工作。使用线程池可以更方便地管理一组并发任务。
  7. 避免长时间持有 GIL 的操作: 如果可能,将 CPU 密集型计算部分剥离到其他进程、使用 C 扩展或利用 NumPy 等库。
  8. 测试并发代码: 并发问题往往难以重现,需要仔细设计测试用例,并多次运行以发现潜在的竞态条件或死锁。

Python 的多线程是实现并发的一种强大工具,尤其在处理 I/O 密集型任务时能显著提升效率。掌握其基本概念、同步机制以及 GIL 的影响,是编写健壮、高效并发程序的关键一步。随着实践的深入,你会对何时以及如何更有效地利用 Python 的并发能力有更深刻的理解。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部