【Python学习】进程和线程
目录
三、Python中的进程实现:multiprocessing模块
4.4 线程池:concurrent.futures.ThreadPoolExecutor
一、前言:进程与线程的核心定位
在Python编程中,进程(Process)和线程(Thread)是实现“并发编程”的核心技术,用于解决“多任务同时执行”的需求——比如同时下载文件、处理数据、响应用户操作等。很多初学者会混淆两者,核心区别在于:进程是操作系统资源分配的最小单位,线程是CPU调度的最小单位。
简单类比:进程相当于一个独立的“工厂”,有自己的厂房(内存空间、资源);线程相当于工厂里的“工人”,共享厂房的资源,同时执行不同的任务。一个工厂(进程)可以有多个工人(线程),多个工厂(进程)之间相互独立,资源不共享。
本教程将从“概念→实现→区别→应用→实战”逐步展开,全程搭配可直接运行的Python代码,兼顾理论与实操,适合Python初学者及需要掌握并发编程的开发者。

二、核心概念:进程与线程的本质区别
2.1 进程(Process)
进程是操作系统进行资源分配和调度的基本单位,每个进程都有自己独立的内存空间、文件描述符、进程ID(PID),进程之间相互独立,互不干扰。
比如:你打开的Python解释器、浏览器、微信,都是独立的进程。即使其中一个进程崩溃,其他进程也不会受到影响(比如浏览器崩溃,微信依然能正常使用)。
进程的特点:
-
独立资源:每个进程有自己的内存、CPU时间片、文件句柄,资源消耗较大。
-
互不干扰:进程间通信(IPC)需要借助特定机制(如队列、管道、共享内存),不能直接访问对方的内存。
-
启动速度慢:创建进程需要操作系统分配资源,比线程启动慢。
2.2 线程(Thread)
线程是进程内的一个执行单元,是CPU调度的最小单位,多个线程共享所属进程的内存空间、资源(如文件句柄、全局变量),线程之间切换速度快。
比如:浏览器的一个进程中,有“渲染页面”“下载文件”“响应点击”等多个线程,这些线程共享浏览器的内存(如缓存的网页数据),协同完成浏览器的功能。
线程的特点:
-
共享资源:同一进程内的线程共享进程的内存、资源,资源消耗小。
-
相互影响:一个线程崩溃可能导致整个进程崩溃(比如浏览器的渲染线程崩溃,浏览器会卡死)。
-
启动速度快:创建线程无需分配独立资源,仅需分配少量栈空间,启动和切换速度远快于进程。
2.3 进程与线程核心区别(表格对比)
|
对比维度 |
进程(Process) |
线程(Thread) |
|---|---|---|
|
资源分配 |
独立资源(内存、PID等) |
共享所属进程的资源 |
|
调度单位 |
操作系统调度(资源分配单位) |
CPU调度(执行单元) |
|
启动速度 |
慢(需分配资源) |
快(仅分配栈空间) |
|
通信难度 |
难(需IPC机制) |
易(共享内存、全局变量) |
|
稳定性 |
高(进程崩溃不影响其他进程) |
低(线程崩溃可能导致进程崩溃) |
|
资源消耗 |
大 |
小 |
三、Python中的进程实现:multiprocessing模块
Python内置multiprocessing模块,用于实现多进程编程,支持创建进程、进程间通信、进程池等功能,解决了Python中“GIL锁”导致的多线程无法利用多核CPU的问题(后续会讲解GIL)。
3.1 基础用法:创建单个进程
使用multiprocessing.Process类创建进程,核心参数:
-
target:进程要执行的函数(必填)。 -
args:传递给函数的参数(元组类型)。 -
kwargs:传递给函数的关键字参数(字典类型)。 -
name:进程名称(可选)。
示例代码(可直接运行):
import multiprocessing
import time
# 定义进程要执行的函数
def task(name, delay):
for i in range(3):
print(f"进程{name}:执行第{i+1}次,当前时间:{time.ctime()}")
time.sleep(delay) # 模拟任务耗时
if __name__ == "__main__":
# 创建进程(target指定函数,args传递参数)
p1 = multiprocessing.Process(target=task, args=("进程1", 1), name="p1")
p2 = multiprocessing.Process(target=task, args=("进程2", 2), name="p2")
# 启动进程
p1.start()
p2.start()
# 等待进程执行完成(阻塞主进程,直到子进程结束)
p1.join()
p2.join()
print("所有进程执行完毕!")
运行结果说明:两个进程会交替执行(因为CPU调度),进程1每1秒执行一次,进程2每2秒执行一次,主进程会等待两个子进程执行完再输出“所有进程执行完毕”。
关键注意点:if __name__ == "__main__": 必须加!因为Windows系统中,创建子进程时会重新导入当前模块,不加这句话会导致无限递归创建进程,报错。
3.2 进阶用法:进程池(Pool)
当需要创建大量进程时,手动创建多个Process对象效率低,且会消耗大量资源。此时可以使用multiprocessing.Pool创建进程池,实现进程的复用和管理。
进程池的核心方法:
-
Pool(n):创建一个包含n个进程的进程池(n通常设为CPU核心数,可通过multiprocessing.cpu_count()获取)。 -
apply_async(func, args):异步提交任务(非阻塞,主进程继续执行,子进程并行执行)。 -
close():关闭进程池,不再接受新任务。 -
join():等待进程池内所有任务执行完毕。
示例代码(批量处理任务):
import multiprocessing
import time
# 定义任务函数(处理单个数据)
def process_data(data):
time.sleep(1) # 模拟数据处理耗时
return f"数据{data}处理完成,结果:{data * 2}"
if __name__ == "__main__":
# 获取CPU核心数,创建对应数量的进程池
cpu_num = multiprocessing.cpu_count()
pool = multiprocessing.Pool(cpu_num)
# 批量提交任务(处理10个数据)
data_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 异步提交,返回结果对象列表
result_list = [pool.apply_async(process_data, args=(data,)) for data in data_list]
# 关闭进程池,等待所有任务完成
pool.close()
pool.join()
# 获取所有任务结果
for result in result_list:
print(result.get()) # get()方法获取任务返回值
print("所有数据处理完毕!")
运行结果说明:进程池会根据CPU核心数分配进程,并行处理10个数据,总耗时约1秒(而非10秒),大幅提升效率。
3.3 进程间通信(IPC)
进程之间相互独立,无法直接访问对方的内存,因此需要借助IPC机制实现通信。Python中常用的IPC方式有3种:队列(Queue)、管道(Pipe)、共享内存(Value/Array)。
3.3.1 队列(Queue):最常用,线程安全
队列是“先进先出(FIFO)”的数据结构,multiprocessing.Queue支持多进程间的安全通信,适合多个进程之间传递数据。
import multiprocessing
import time
# 生产者进程:向队列中放入数据
def producer(queue):
for i in range(5):
data = f"数据{i+1}"
queue.put(data) # 放入队列
print(f"生产者:放入{data},当前队列大小:{queue.qsize()}")
time.sleep(0.5)
# 消费者进程:从队列中取出数据
def consumer(queue):
while True:
if not queue.empty(): # 判断队列是否为空
data = queue.get() # 取出数据
print(f"消费者:取出{data},当前队列大小:{queue.qsize()}")
time.sleep(1)
else:
break # 队列空了,退出循环
if __name__ == "__main__":
# 创建队列(默认无界队列,可指定maxsize设置最大容量)
queue = multiprocessing.Queue()
# 创建生产者和消费者进程
p_producer = multiprocessing.Process(target=producer, args=(queue,))
p_consumer = multiprocessing.Process(target=consumer, args=(queue,))
# 启动进程
p_producer.start()
p_producer.join() # 等待生产者放入所有数据
p_consumer.start()
p_consumer.join()
print("通信完成!")
3.3.2 管道(Pipe):适合两个进程间通信
管道通过multiprocessing.Pipe()创建,返回两个连接对象(conn1, conn2),两个进程分别通过这两个对象发送和接收数据,支持双向通信。
import multiprocessing
import time
# 进程1:发送数据
def send_data(conn):
data_list = ["a", "b", "c", "d"]
for data in data_list:
conn.send(data) # 发送数据
print(f"进程1:发送{data}")
time.sleep(0.5)
conn.close() # 关闭连接
# 进程2:接收数据
def recv_data(conn):
while True:
try:
data = conn.recv() # 接收数据
print(f"进程2:接收{data}")
time.sleep(1)
except EOFError:
break # 管道关闭,退出循环
if __name__ == "__main__":
# 创建管道(默认双向通信,duplex=False表示单向)
conn1, conn2 = multiprocessing.Pipe()
# 创建两个进程
p1 = multiprocessing.Process(target=send_data, args=(conn1,))
p2 = multiprocessing.Process(target=recv_data, args=(conn2,))
p1.start()
p2.start()
p1.join()
p2.join()
print("管道通信完成!")
四、Python中的线程实现:threading模块
Python内置threading模块,用于实现多线程编程,线程共享进程资源,启动速度快,适合处理IO密集型任务(如网络请求、文件读写)。
注意:Python存在“GIL锁(全局解释器锁)”,导致同一时刻只有一个线程能执行Python字节码,因此多线程无法利用多核CPU,适合IO密集型任务,而非CPU密集型任务(CPU密集型任务建议用多进程)。
4.1 基础用法:创建单个线程
使用threading.Thread类创建线程,核心参数与Process类类似:target(执行函数)、args(参数元组)、kwargs(关键字参数)、name(线程名称)。
import threading
import time
# 定义线程要执行的函数
def task(name, delay):
for i in range(3):
print(f"线程{name}:执行第{i+1}次,当前时间:{time.ctime()}")
time.sleep(delay)
if __name__ == "__main__":
# 创建线程
t1 = threading.Thread(target=task, args=("线程1", 1), name="t1")
t2 = threading.Thread(target=task, args=("线程2", 2), name="t2")
# 启动线程
t1.start()
t2.start()
# 等待线程执行完成(阻塞主进程)
t1.join()
t2.join()
print("所有线程执行完毕!")
运行结果说明:两个线程交替执行,与多进程类似,但启动速度更快,资源消耗更小。
4.2 线程的核心特性:守护线程(Daemon)
默认情况下,主进程会等待所有子线程执行完毕后才退出;如果将线程设置为“守护线程”(daemon=True),主进程退出时,守护线程会被强制终止,无论是否执行完毕。
应用场景:后台任务(如日志记录、心跳检测),主进程退出后,后台任务无需继续执行。
import threading
import time
def daemon_task():
while True:
print("守护线程:正在运行...")
time.sleep(1)
def normal_task():
for i in range(3):
print("普通线程:执行第{i+1}次")
time.sleep(1)
if __name__ == "__main__":
# 创建守护线程(daemon=True)
daemon_thread = threading.Thread(target=daemon_task, daemon=True)
# 创建普通线程
normal_thread = threading.Thread(target=normal_task)
daemon_thread.start()
normal_thread.start()
# 等待普通线程执行完毕(主进程会等待普通线程,但不会等待守护线程)
normal_thread.join()
print("主进程退出!")
运行结果说明:普通线程执行3次后结束,主进程退出,此时守护线程被强制终止,不再打印“守护线程:正在运行...”。
4.3 线程安全问题:锁(Lock)
多个线程共享进程的全局变量、资源时,可能出现“竞态条件”(多个线程同时修改同一个资源,导致数据错误)。此时需要使用threading.Lock(锁)来保证线程安全,确保同一时刻只有一个线程能访问共享资源。
示例:未加锁导致的数据错误 vs 加锁后的数据正确
import threading
import time
# 全局变量(共享资源)
count = 0
# 未加锁的任务函数(会出现数据错误)
def task_without_lock():
global count
for _ in range(100000):
count += 1 # 多个线程同时修改count,会出现竞态条件
# 加锁的任务函数(线程安全)
def task_with_lock(lock):
global count
for _ in range(100000):
lock.acquire() # 获取锁(阻塞,直到拿到锁)
count += 1
lock.release() # 释放锁(必须释放,否则会导致死锁)
if __name__ == "__main__":
# 1. 未加锁测试
count = 0
t1 = threading.Thread(target=task_without_lock)
t2 = threading.Thread(target=task_without_lock)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"未加锁:count = {count}") # 结果通常小于200000(数据错误)
# 2. 加锁测试
count = 0
lock = threading.Lock() # 创建锁对象
t3 = threading.Thread(target=task_with_lock, args=(lock,))
t4 = threading.Thread(target=task_with_lock, args=(lock,))
t3.start()
t4.start()
t3.join()
t4.join()
print(f"加锁:count = {count}") # 结果一定是200000(数据正确)
关键注意点:锁的acquire()和release()必须成对出现,否则会导致死锁(线程一直等待锁释放,无法继续执行)。也可以使用with lock:上下文管理器,自动释放锁,更安全:
def task_with_lock(lock):
global count
for _ in range(100000):
with lock: # 自动acquire()和release()
count += 1
4.4 线程池:concurrent.futures.ThreadPoolExecutor
与进程池类似,线程池用于管理大量线程,避免频繁创建和销毁线程带来的开销。Python 3.2+ 提供concurrent.futures.ThreadPoolExecutor,用法与进程池类似。
from concurrent.futures import ThreadPoolExecutor
import time
# 定义任务函数
def process_data(data):
time.sleep(1)
return f"数据{data}处理完成,结果:{data * 2}"
if __name__ == "__main__":
# 创建线程池(max_workers指定线程数量)
with ThreadPoolExecutor(max_workers=4) as executor:
# 批量提交任务
data_list = [1, 2, 3, 4, 5, 6, 7, 8]
# 异步提交,返回Future对象列表
futures = [executor.submit(process_data, data) for data in data_list]
# 获取所有任务结果
for future in futures:
print(future.result()) # result()方法获取返回值
print("所有任务处理完毕!")
说明:with语句会自动关闭线程池,无需手动调用close()和join(),更简洁。
五、关键补充:GIL锁(全局解释器锁)
5.1 GIL锁是什么?
GIL锁是Python解释器(CPython)的一个全局锁,用于保证同一时刻只有一个线程能执行Python字节码。这是CPython的设计限制,目的是简化内存管理(避免多线程同时操作内存导致的冲突)。
5.2 GIL锁的影响
-
对IO密集型任务:影响不大。因为IO操作(如网络请求、文件读写)会阻塞线程,此时GIL锁会释放,其他线程可以执行,因此多线程能提升效率。
-
对CPU密集型任务:影响很大。因为CPU密集型任务(如计算、循环)会一直占用CPU,GIL锁无法释放,导致多线程只能串行执行,无法利用多核CPU,效率甚至不如单线程。此时建议使用多进程(进程有独立的GIL锁,可利用多核)。
5.3 如何规避GIL锁的限制?
-
CPU密集型任务:使用
multiprocessing多进程,或使用Cython、NumPy等避开Python字节码的库。 -
IO密集型任务:使用
threading多线程,或使用asyncio异步编程(后续可扩展)。
六、进程与线程的选择场景(实战指南)
实际开发中,选择进程还是线程,核心看任务类型和需求,总结如下:
6.1 选择多进程的场景
-
CPU密集型任务:如数据计算、图像处理、复杂算法(多进程可利用多核CPU,提升效率)。
-
任务之间相互独立,无需频繁通信:如批量处理多个独立文件、并行计算多个任务。
-
需要高稳定性:一个任务崩溃不影响其他任务(进程独立,互不干扰)。
6.2 选择多线程的场景
-
IO密集型任务:如网络爬虫、文件读写、接口请求(线程切换快,能利用IO阻塞的时间执行其他任务)。
-
任务之间需要频繁通信、共享资源:如实时数据处理、UI界面响应(线程共享内存,通信方便)。
-
对资源消耗敏感,需要快速启动:如后台轻量任务、实时监控。
6.3 总结对比
|
任务类型 |
推荐方案 |
原因 |
|---|---|---|
|
CPU密集型(计算多) |
多进程 |
规避GIL锁,利用多核CPU提升效率 |
|
IO密集型(等待多) |
多线程 |
线程切换快,利用等待时间并行执行 |
|
任务独立、高稳定 |
多进程 |
进程独立,一个崩溃不影响其他 |
|
频繁通信、共享资源 |
多线程 |
共享内存,通信方便、开销小 |
七、实战案例:多进程+多线程综合应用
需求:批量下载10张图片,使用多进程分配任务(每个进程处理2张图片),每个进程内部使用多线程并行下载图片(提升单进程内的下载效率)。
依赖库:requests(用于下载图片),需提前安装:pip install requests
import multiprocessing
from concurrent.futures import ThreadPoolExecutor
import requests
import os
# 图片下载函数(线程执行)
def download_image(url, save_path):
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # 抛出HTTP错误
with open(save_path, "wb") as f:
f.write(response.content)
print(f"成功下载:{os.path.basename(save_path)}")
except Exception as e:
print(f"下载失败:{url},错误:{str(e)}")
# 进程任务函数(每个进程处理一批图片)
def process_batch(image_urls, save_dir):
# 创建线程池,每个进程内启动2个线程
with ThreadPoolExecutor(max_workers=2) as executor:
# 批量提交下载任务
futures = []
for i, url in enumerate(image_urls):
save_path = os.path.join(save_dir, f"image_{i+1}.jpg")
futures.append(executor.submit(download_image, url, save_path))
# 等待所有线程完成
for future in futures:
future.result()
if __name__ == "__main__":
# 模拟10张图片URL(实际可替换为真实URL)
image_urls = [
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg",
"https://example.com/image4.jpg",
"https://example.com/image5.jpg",
"https://example.com/image6.jpg",
"https://example.com/image7.jpg",
"https://example.com/image8.jpg",
"https://example.com/image9.jpg",
"https://example.com/image10.jpg"
]
# 创建保存目录
save_dir = "downloaded_images"
os.makedirs(save_dir, exist_ok=True)
# 拆分任务:5个进程,每个进程处理2张图片
batch_size = 2
batches = [image_urls[i:i+batch_size] for i in range(0, len(image_urls), batch_size)]
# 创建进程池,启动5个进程
with multiprocessing.Pool(processes=5) as pool:
# 批量提交进程任务
futures = [pool.apply_async(process_batch, args=(batch, save_dir)) for batch in batches]
# 等待所有进程完成
for future in futures:
future.get()
print("所有图片下载完成!")
案例说明:结合多进程和多线程的优势,多进程利用多核CPU分配任务,多线程在每个进程内并行处理IO密集型的下载任务,大幅提升整体下载效率。
八、常见问题与避坑指南
8.1 进程/线程创建过多导致资源耗尽
解决方案:使用进程池/线程池,限制进程/线程数量(通常设为CPU核心数或2-4倍,避免过多占用资源)。
8.2 死锁问题
原因:多个线程/进程相互等待对方释放锁,导致无法继续执行。
解决方案:
-
确保锁的
acquire()和release()成对出现,优先使用with lock:上下文管理器。 -
多个锁的获取顺序保持一致(如先获取锁A,再获取锁B,所有线程都遵循此顺序)。
8.3 多线程无法利用多核CPU
原因:GIL锁限制,同一时刻只有一个线程执行Python字节码。
解决方案:CPU密集型任务改用多进程,或使用异步编程。
8.4 进程间通信数据丢失
原因:队列/管道的容量有限,或未正确关闭连接。
解决方案:
-
使用队列时,避免放入超过队列容量的数据,可通过
qsize()判断队列大小。 -
管道通信后,及时关闭连接(
conn.close()),避免资源泄漏。
九、总结与拓展
9.1 核心总结
-
进程是资源分配单位,线程是CPU调度单位,线程共享进程资源,进程相互独立。
-
多进程适合CPU密集型任务,多线程适合IO密集型任务,规避GIL锁的限制。
-
进程间通信需用IPC机制(队列、管道),线程间通信可直接共享资源,但需注意线程安全(加锁)。
-
进程池/线程池可提升效率,避免频繁创建/销毁进程/线程带来的开销。
9.2 拓展学习
-
异步编程(
asyncio):更高效的IO密集型任务解决方案,基于事件循环,比多线程更轻量。 -
分布式进程(
multiprocessing.Manager):实现多台机器之间的进程通信,用于分布式计算。 -
线程同步高级用法:
RLock(可重入锁)、Condition(条件变量)、Semaphore(信号量)。
通过本教程的学习,你已经掌握了Python进程和线程的核心用法、区别及应用场景,结合实操案例多练习,就能熟练运用并发编程解决实际问题。
更多推荐



所有评论(0)