文章目录
-
- 一、多任务的基础认知
-
- [1. 多任务的定义](#1. 多任务的定义)
- [2. 多任务的两种表现形式](#2. 多任务的两种表现形式)
- 二、进程:资源分配的最小单位
-
- [1. 进程的基本概念](#1. 进程的基本概念)
- [2. 主进程与子进程](#2. 主进程与子进程)
- 三、Python中创建多进程的核心步骤
-
- [1. 多进程实现的三大步骤](#1. 多进程实现的三大步骤)
- [2. 基础示例:多进程执行任务](#2. 基础示例:多进程执行任务)
- 四、多进程的核心管理技巧
-
- [1. 进程的阻塞与同步:join()方法](#1. 进程的阻塞与同步:join()方法)
- [2. 进程的同步控制:进程锁(Lock)](#2. 进程的同步控制:进程锁(Lock))
- [3. 进程编号的获取:pid与ppid](#3. 进程编号的获取:pid与ppid)
- [4. 必知的魔术方法:if name == 'main'](#4. 必知的魔术方法:if name == 'main')
- 五、进程池:高效管理大量子进程
-
- [1. 进程池的核心思想](#1. 进程池的核心思想)
- [2. 进程池的适用场景](#2. 进程池的适用场景)
- [3. 进程池的核心类与方法](#3. 进程池的核心类与方法)
- [4. 进程池的使用示例](#4. 进程池的使用示例)
- 六、多进程编程的核心注意事项
-
- [1. 进程间不共享全局资源](#1. 进程间不共享全局资源)
- [2. 主进程与子进程的结束顺序](#2. 主进程与子进程的结束顺序)
- 七、总结
在Python开发中,当我们需要同时处理多个任务时------比如一边爬取网页数据,一边将已爬取的数据写入数据库,单线程的串行执行就会显得效率低下。这时候, 多任务编程 就成了提升程序性能的关键手段。
进程作为操作系统资源分配的最小单位,是实现多任务的核心方式之一。本文将从多任务的基本概念出发,系统梳理Python中进程的创建、管理、同步控制以及进程池的使用技巧,帮你快速掌握进程级多任务编程的核心知识。
一、多任务的基础认知
在深入学习进程之前,我们首先要搞清楚:什么是多任务?它有哪些表现形式?
1. 多任务的定义
多任务是指在同一时间内执行多个任务。在Python中,实现多任务的方式主要有三种:
-
进程:操作系统资源分配的最小单位,每个进程拥有独立的内存空间。
-
线程:CPU调度的最小单位,线程共享所属进程的资源。
-
协程:用户态的轻量级线程,由程序自身控制调度,开销最小。
2. 多任务的两种表现形式
多任务的执行看似是"同时进行",但实际上分为并发 和并行两种核心模式,二者的底层逻辑有本质区别:
| 表现形式 | 核心逻辑 | 硬件要求 | 示例 |
|---|---|---|---|
| 并发 | 多个任务交替抢占CPU资源,在宏观上表现为"同时执行",微观上是串行执行 | 可在单CPU核心下实现 | 单核心CPU上,多个应用程序交替运行 |
| 并行 | 多个任务真正同时执行,每个任务占用独立的CPU核心 | 必须依赖多CPU核心 | 多核心CPU上,多个任务在不同核心上同时运行 |
| 简单来说:并发是"交替执行",并行是"同时执行"。 |
二、进程:资源分配的最小单位
1. 进程的基本概念
一个正在运行的程序就是一个进程。比如我们打开Python解释器运行脚本,就会创建一个Python进程;在脚本中新建任务时,又会产生子进程。
进程的核心特性是独立性:每个进程都拥有独立的内存空间、文件描述符等系统资源,进程之间默认不共享资源。
2. 主进程与子进程
-
主进程:程序启动时默认创建的进程,是程序的入口和核心,负责管理子进程。
-
子进程:程序运行过程中,通过进程模块新建的进程,用于执行特定的子任务。
重要注意点 :正常情况下,主进程的优先级高于子进程,但子进程的执行时机并非绝对可控。因为进程的调度权由操作系统内核决定,内核会根据时间片、进程优先级等因素分配CPU资源,可能出现子进程启动后立即执行的情况。
三、Python中创建多进程的核心步骤
Python的multiprocessing模块是实现多进程编程的核心工具,通过它可以轻松创建和管理子进程。
1. 多进程实现的三大步骤
步骤1:导入进程包
python
import multiprocessing
步骤2:创建子进程对象
通过multiprocessing.Process类创建子进程对象,核心参数如下:
python
进程对象 = multiprocessing.Process(
group=None, # 进程组,目前仅支持None
target=None, # 要执行的目标任务函数名(**不能带括号**,否则会直接调用函数而非交给子进程执行)
name=None, # 进程名,可选,用于标识进程
args=(), # 传递给目标函数的位置参数,**必须是元组类型**(单个参数需加逗号,如(10,))
kwargs={} # 传递给目标函数的关键字参数,**必须是字典类型**,如{'a': 1, 'b': 2}
)
关键提醒 :target参数必须是函数名本身,不能带括号。如果写成target=func(),会直接在主进程中执行函数,而非交给子进程执行。
步骤3:启动子进程
通过start()方法启动子进程,此时操作系统会为子进程分配资源并执行目标函数。
python
进程对象.start()
2. 基础示例:多进程执行任务
python
import multiprocessing
import time
# 定义要执行的任务函数
def task(name, delay):
for i in range(3):
print(f"任务{name}执行中:{i}")
time.sleep(delay)
if __name__ == '__main__':
# 创建子进程1:传递位置参数
p1 = multiprocessing.Process(target=task, args=("A", 1))
# 创建子进程2:传递关键字参数
p2 = multiprocessing.Process(target=task, kwargs={"name": "B", "delay": 0.5})
# 启动子进程
p1.start()
p2.start()
运行上述代码,你会看到两个任务交替执行,这就是多进程实现的多任务效果。
四、多进程的核心管理技巧
创建子进程后,我们还需要掌握进程的同步、阻塞、编号获取等核心管理技巧,才能更好地控制多任务的执行流程。
1. 进程的阻塞与同步:join()方法
join()方法的作用是阻塞当前进程 ,直到调用join()的子进程执行完毕后,再继续执行后续代码。这是实现进程同步的关键方法。
基础用法:等待单个子进程完成
python
p1.start()
p1.join() # 主进程阻塞,直到p1执行完毕
print("p1执行完毕,开始执行后续代码")
进阶用法:确保子进程按顺序执行
如果需要让多个子进程按顺序执行 (而非交替执行),可以在创建每个子进程后,立即调用start()和join()方法。
python
p1.start()
p1.join() # 等待p1完成
p2.start()
p2.join() # 等待p2完成
2. 进程的同步控制:进程锁(Lock)
当多个进程需要访问共享资源 (如同一文件、同一设备)时,可能会出现资源竞争问题,导致数据错乱。此时需要使用进程锁来保证同一时间只有一个进程能访问共享资源。
进程锁的核心特性
-
互斥性:同一时间,只有一个进程能成功获取锁,其他进程尝试获取锁会进入阻塞状态。
-
阻塞性 :当锁被其他进程持有时,
lock.acquire()会让当前进程暂停执行,直到锁被释放。
进程锁的使用步骤
-
创建锁对象 :
m1_lock = multiprocessing.Lock() -
传递锁对象 :通过
args参数将锁传递给子进程(不能使用全局变量共享,因为进程间资源独立)。 -
获取锁与释放锁:
-
lock.acquire():获取锁(锁上门),如果锁已被占用,则阻塞。 -
lock.release():释放锁(打开门),唤醒等待锁的进程。
-
进程锁的使用示例
python
import multiprocessing
import time
def task(lock, name):
lock.acquire() # 获取锁
try:
for i in range(2):
print(f"任务{name}独占资源执行:{i}")
time.sleep(0.5)
finally:
lock.release() # 释放锁,确保即使出现异常也能释放锁
if __name__ == '__main__':
lock = multiprocessing.Lock()
p1 = multiprocessing.Process(target=task, args=(lock, "A"))
p2 = multiprocessing.Process(target=task, args=(lock, "B"))
p1.start()
p2.start()
重要提醒 :一个进程锁只能被锁定一次 ,如果同一进程尝试两次调用acquire(),会导致进程死锁(卡住),只有释放锁后才能再次获取。
3. 进程编号的获取:pid与ppid
为了方便管理进程(如查看进程状态、终止进程),我们可以通过os模块获取进程的编号。
-
os.getpid():获取当前进程的编号(PID)。 -
os.getppid():获取当前进程的父进程的编号(PPID)。
示例:获取进程编号
python
import multiprocessing
import os
def task():
print(f"子进程PID:{os.getpid()}")
print(f"子进程的父进程PPID:{os.getppid()}")
if __name__ == '__main__':
print(f"主进程PID:{os.getpid()}")
p = multiprocessing.Process(target=task)
p.start()
4. 必知的魔术方法:if name == 'main'
在Windows系统中,使用multiprocessing模块创建子进程时,if __name__ == '__main__': 必须将进程创建的代码放在语句块中,否则会导致程序无限递归创建子进程,最终报错。
核心作用
-
区分主进程与子进程 :Windows系统在创建子进程时,会重新导入主模块,
__name__变量在主进程中为'__main__',在子进程中为模块名,从而避免重复执行进程创建代码。 -
支持模块导入:当模块被其他脚本导入时,不会执行进程创建的代码,保证模块的可复用性。
正确写法示例
python
import multiprocessing
def task():
print("子进程执行中")
if __name__ == '__main__':
# 仅在主进程中创建子进程
p = multiprocessing.Process(target=task)
p.start()
补充 :在Linux/Mac系统中,由于使用fork机制创建子进程,无需强制使用该语句块,但为了代码的跨平台兼容性,强烈推荐始终使用。
五、进程池:高效管理大量子进程
当需要执行大量短时间任务 时,频繁创建和销毁子进程会带来巨大的性能开销。此时,进程池(Pool)是更优的选择。
1. 进程池的核心思想
进程池会预先创建一批固定数量的子进程,放入"池子"中复用。当有新任务到来时,直接分配给池中的空闲进程执行;任务完成后,进程不会被销毁,而是等待下一个任务。这样可以有效避免频繁创建/销毁进程的性能损耗。
2. 进程池的适用场景
-
任务数量多、单个任务执行时间短。
-
任务之间相互独立,可并行执行。
-
需要控制并发进程数量,防止CPU资源耗尽。
3. 进程池的核心类与方法
进程池的核心类是multiprocessing.Pool,常用方法如下:
-
apply_async(func, args=(), kwargs={}):异步提交任务(推荐),不会阻塞主进程,任务完成后可通过返回值获取结果。 -
apply(func, args=(), kwargs={}):同步提交任务,会阻塞主进程,直到任务执行完毕,不推荐用于多任务场景。 -
map(func, iterable):批量提交简单任务,将可迭代对象中的每个元素作为参数传递给函数。 -
close():关闭进程池,不再接收新任务。 -
join():阻塞主进程,等待进程池中所有任务执行完毕。
4. 进程池的使用示例
python
import multiprocessing
import os
import time
def task(num):
time.sleep(1)
return f"任务{num}执行完毕,进程PID:{os.getpid()}"
if __name__ == '__main__':
# 创建进程池,设置最大进程数为4(建议等于CPU核心数)
pool = multiprocessing.Pool(processes=4)
# 异步提交10个任务
results = []
for i in range(10):
res = pool.apply_async(task, args=(i,))
results.append(res)
# 关闭进程池,不再接收新任务
pool.close()
# 等待所有任务执行完毕
pool.join()
# 获取所有任务的结果
for res in results:
print(res.get())
六、多进程编程的核心注意事项
1. 进程间不共享全局资源
每个进程都有独立的内存空间,子进程会拷贝主进程的资源,但修改子进程中的全局变量不会影响主进程,反之亦然。
如果需要实现进程间的通信,可以使用multiprocessing模块提供的队列(Queue)、**管道(Pipe)**等工具。
2. 主进程与子进程的结束顺序
默认情况下,主进程会等待所有子进程执行完毕后才会结束。如果需要让主进程结束时,子进程也立即终止,可以采用以下两种方案:
方案1:设置守护进程
通过将子进程的daemon属性设置为True,使子进程成为守护进程。主进程结束时,守护进程会被立即销毁,不再执行剩余代码。
python
p = multiprocessing.Process(target=task)
p.daemon = True # 设置为守护进程
p.start()
方案2:手动终止子进程
在主进程结束前,调用子进程的terminate()方法,手动终止子进程的执行。
python
p = multiprocessing.Process(target=task)
p.start()
# 主进程执行其他任务
time.sleep(2)
# 终止子进程
p.terminate()
print("主进程执行完毕,子进程已终止")
七、总结
进程作为Python多任务编程的重要方式,适用于CPU密集型任务(如数据计算、图像处理),通过充分利用多核心CPU资源,可以显著提升程序的执行效率。
本文梳理了进程的基本概念、创建步骤、管理技巧以及进程池的使用方法,核心要点如下:
-
多任务分为并发和并行两种表现形式,进程是实现多任务的核心方式之一。
-
使用
multiprocessing.Process创建子进程,需注意target参数的写法和if __name__ == '__main__'的必要性。 -
通过
join()实现进程同步,通过Lock解决资源竞争问题。 -
进程池适用于大量短时间任务,可有效避免频繁创建/销毁进程的性能开销。
-
进程间默认不共享资源,主进程与子进程的结束顺序可通过守护进程
daemon或terminate()方法控制。