Python 多线程
Python 线程是通过 threading 模块实现的,用于在程序中执行并发任务,适合处理 I/O 密集型操作。 由于全局解释器锁(GIL)的限制,Python 线程无法充分利用多核 CPU 进行并行计算。 线程常用于需要快速响应用户交互或异步处理的任务,如网络请求或文件操作。
1. 线程与进程的关系
在学习多线程之前,我们需要先理解线程和进程的基本概念及其关系。
1.1 定义
- 进程(Process):是操作系统分配资源(如CPU、内存)的最小单位。每个进程拥有独立的内存空间,进程之间通常是相互隔离的。
- 线程(Thread):是进程内部的一个执行单元。一个进程可以包含多个线程,线程共享进程的内存空间和资源。
1.2 关系
- 一个进程至少包含一个线程,通常称为主线程。
- 多线程共享进程的内存和资源,因此线程间的通信和数据共享比进程间更简单高效。
- 线程的创建和销毁开销比进程小,因为线程不需要分配独立的内存空间。
1.3 Python中的体现
- Python使用
threading
模块来创建和管理线程。 - Python使用
multiprocessing
模块来创建和管理进程。 - 由于Python的全局解释器锁(GIL),多线程在CPU密集型任务中无法充分利用多核CPU,但在I/O密集型任务中表现良好。
2. 多线程的基本使用
让我们从最简单的线程创建开始,逐步掌握Python中线程的基本操作。
Python的threading
模块提供了Thread
类来创建线程。可以通过指定target
参数来定义线程要执行的任务。
以下是一个完整的示例:
import threading
import time
def worker(arg):
print(f"线程开始运行, arg:{arg}")
time.sleep(2) # 模拟耗时任务
print("线程运行结束")
# 创建线程
thread = threading.Thread(target=worker, args=(1,))
# 启动线程
thread.start()
# 等待线程结束
thread.join()
print("主线程结束")
运行结果:
线程开始运行, arg:1
线程运行结束
主线程结束
start()
:启动线程,开始执行target
指定的函数。join()
:等待线程执行完成,主线程才会继续运行。- 每个线程独立运行,但主线程可以通过
join()
控制执行顺序。
3. 线程池
当需要执行大量短期任务时,频繁创建和销毁线程会带来性能开销。线程池通过复用线程解决了这个问题。
- 线程池预先创建一组线程,任务提交时从池中分配线程执行,任务完成后线程返回池中待命。
- 适用于处理多个并行任务,如批量文件处理或网络请求。
Python的concurrent.futures
模块提供了ThreadPoolExecutor
类来实现线程池。
以下是一个完整示例:
from concurrent.futures import ThreadPoolExecutor
import time
def task(name):
print(f"任务 {name} 开始")
time.sleep(2) # 模拟耗时任务
print(f"任务 {name} 结束")
# 创建线程池,最多3个线程
with ThreadPoolExecutor(max_workers=3) as executor:
# 提交任务
executor.submit(task, "A")
executor.submit(task, "B")
executor.submit(task, "C")
print("所有任务已提交")
运行结果:
任务 A 开始
任务 B 开始
任务 C 开始
(等待2秒)
任务 A 结束
任务 B 结束
任务 C 结束
所有任务已提交
max_workers
:指定线程池中线程的最大数量。submit()
:将任务提交给线程池,返回一个Future
对象(可用于获取任务结果,此处未展示)。
4. 锁与线程安全
多线程共享资源时,如果不加以保护,可能导致数据不一致。锁是确保线程安全的关键工具。
当多个线程同时修改共享资源(如全局变量)时,可能出现竞争条件(Race Condition),导致结果不可预测。
threading.Lock
:互斥锁,确保同一时间只有一个线程访问共享资源。threading.RLock
:可重入锁,允许同一线程多次获取锁。threading.Semaphore
:信号量,控制多个线程的访问数量。
以下是一个使用锁的完整示例:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
with lock: # 获取锁,保护共享资源
counter += 1
# 创建10个线程
threads = []
for _ in range(10):
thread = threading.Thread(target=increment)
threads.append(thread)
thread.start()
# 等待所有线程结束
for thread in threads:
thread.join()
print(f"最终计数器值: {counter}")
运行结果:
最终计数器值: 10
with lock
:自动获取和释放锁,防止忘记释放导致死锁。- 未加锁时,
counter
可能小于10,因为多个线程同时操作会覆盖彼此的修改。
5. 同时使用多进程和多线程
在某些复杂场景下,可能需要结合多进程和多线程来充分利用系统资源。
5.1 适用场景
- CPU密集型任务:使用多进程绕过GIL限制,利用多核CPU。
- I/O密集型任务:在每个进程内使用多线程提高效率。
5.2 注意事项
- GIL限制:Python的多线程受GIL限制,CPU密集型任务应优先使用多进程。
- 资源管理:多进程和多线程混合使用时需注意内存和锁的管理。
- 和多进程一样,涉及到数据共享的时候,也需要锁来保证数据的一致性。
以下是一个完整示例:
from multiprocessing import Process
import threading
import time
def thread_task(name):
print(f"线程 {name} 开始")
time.sleep(2) # 模拟耗时任务
print(f"线程 {name} 结束")
def process_task():
print("进程开始")
threads = []
for i in range(3):
thread = threading.Thread(target=thread_task, args=(i,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print("进程结束")
if __name__ == "__main__":
processes = []
for _ in range(2):
process = Process(target=process_task)
processes.append(process)
process.start()
for process in processes:
process.join()
print("所有进程结束")
运行结果(可能因线程和进程调度顺序而略有不同):
进程开始
线程 0 开始
线程 1 开始
线程 2 开始
进程开始
线程 0 开始
线程 1 开始
线程 2 开始
(等待2秒)
线程 0 结束
线程 1 结束
线程 2 结束
进程结束
线程 0 结束
线程 1 结束
线程 2 结束
进程结束
所有进程结束