Python 进程
一、多任务的概念
多任务是指在同一时间内执行多个任务,例如: 现在电脑安装的操作系统都是多任务操作系统,可以同时运行着多个软件。有以下两种执行方式:
-
并发 Concurrency
-
并行 Parallelism
并发:
在一段时间内交替去执行任务。
例如:
对于单核cpu处理多任务,操作系统轮流让各个软件交替执行,假如:软件1执行0.01秒,切换到软件2,软件2执行0.01秒,再切换到软件3,执行0.01秒……这样反复执行下去。表面上看,每个软件都是交替执行的,但是,由于CPU的执行速度实在是太快了,我们感觉就像这些软件都在同时执行一样,这里需要注意单核cpu是并发的执行多任务的。
并行**:**
对于多核cpu处理多任务,操作系统会给cpu的每个内核安排一个执行的软件,多个内核是真正的一起执行软件。这里需要注意多核cpu是并行的执行多任务,始终有多个软件一起执行。
1、进程和线程的定义
🧩 1. 定义
-
进程 (Process)
-
操作系统中资源分配的最小单位。
-
启动一个程序 → 系统会给它分配独立的一块内存空间(代码区、数据区、堆、栈)。
-
进程之间的数据相互独立,默认不能直接访问。
-
-
线程 (Thread)
-
操作系统中CPU 调度的最小单位。
-
一个进程里可以有多个线程,它们共享进程的内存空间。
-
每个线程有自己独立的执行栈和程序计数器。
-
🧩 2. 关系
-
进程像是一个 工厂(有独立的地皮、设备、电力)。
-
线程像是工厂里的 工人(共享工厂资源,但各自独立干活)。
-
一个工厂至少有一个工人(进程至少有一个线程)。
🧩 3. 区别总结
| 对比点 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 资源 | 拥有独立的内存空间和系统资源 | 共享同一进程的内存和资源 |
| 开销 | 创建/销毁进程开销大 | 创建/销毁线程开销小 |
| 通信 | 进程间通信(IPC)需要借助管道、队列、共享内存等 | 线程间通信可直接读写共享变量 |
| 稳定性 | 一个进程挂了,不会影响其他进程 | 一个线程崩溃,可能导致整个进程崩溃 |
| 调度 | 操作系统调度的基本单位(较少切换) | CPU 调度的基本单位(切换更快) |
🧩 4. 举例
假设你开了一个 浏览器进程:
-
这是一个进程,独立占用内存。
-
里面有很多线程:
-
UI 渲染线程
-
网络请求线程
-
JavaScript 执行线程
-
音视频播放线程
这些线程共享浏览器进程的内存(比如网页数据),但各自运行不同的任务。
-
-
线程池:适合 IO 密集型任务(等待时间多,CPU 空闲)。
-
进程池:适合 CPU 密集型任务(要大量计算)。
通常情况:
-
如果是 网络请求/数据库/文件IO → 线程池就够了。
-
如果是 模型推理/大规模数值计算 → 进程池更合适
进程(Process),顾名思义,就是进行中的程序。进程是python中最小的资源分配单元,进程之间的数据,内存是不共享的,每启动一个进程,都要独立分配资源和拷贝访问的数据,所以进程的启动和销毁的代价是比较大。
Python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并发执行不同的任务。 在同一个进程内的线程的数据是可以进行互相访问的,这点区别于多进程。
一个进程至少要包含一个线程,每个进程在启动的时候就会自动的启动一个线程,进程里面的第一个线程就是主线程,每次在进程内创建的子线程都是由主线程进程创建和销毁,子线程也可以由主线程创建出来的线程创建和销毁线程。
进程与线程的区别
-
线程是执行的指令集,进程是资源的集合;
-
一个程序中默认有一个 主进程 ,一个进程中默认有一个主线程
-
线程的启动速度要比进程的启动速度要快;
-
一个新的线程很容易被创建,一个新的进程创建需要对父进程进行一次克隆;
-
线程共享创建它的进程的内存空间,进程的内存是独立的。
-
两个线程共享的数据都是同一份数据,两个子进程的数据不是共享的,而且数据是独立的;
-
同一个进程的线程之间可以直接交流,同一个主进程的多个子进程之间是不可以进行交流,如果两个进程之间需要通信,就必须要通过一个中间代理来实现;
-
一个线程可以控制和操作同一个进程里的其他线程,线程与线程之间没有隶属关系,但是进程只能操作子进程;
二、创建一个进程
Python提供了跨平台的多进行模块"multiprocessing"。
1、Process类的说明
Process([group [, target [, name [, args [, kwargs]]]]])
-
group:指定进程组,目前只能使用None
-
target:执行的目标任务名
-
name:进程名字
-
args:以元组方式给执行任务传参
-
kwargs:以字典方式给执行任务传参
Process创建的实例对象的常用方法:
-
start():启动子进程实例(创建子进程)
-
join():阻塞当前进程,等待子进程执行结束
-
terminate():不管任务是否完成,立即终止子进程
Porcess对象其他的函数与属性
-
name:进程的名称;
-
daemon:布尔值,是否是守护进程;
-
pid:进程ID;
-
exitcode:进程退出码;
-
run():表示进程所要做的事情,通过向参数taget指定一个函数的方式指定run的行为,或者在子类中重载该方法;
-
is_alive():判断进程是否还活着;
2、直接使用Process创建进程
import os
from multiprocessing import Process
def run_a_sub_proc(name):
print(f'子进程:{name}({os.getpid()})开始...')
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
# 通过对Process类进行实例化创建一个子进程
p = Process(target=run_a_sub_proc, args=('测试进程', ))
p.start()
注意:这里需要明确以下主进程和子进程。当我们通过python demo.py开始执行demo.py这个程序时,程序被赋予了声明,成为一个进程,这个进程是 主进程 。而在主进程执行过程,通过对Process类进行实例化创建的是 子进程 。
3、继承Process类创建进程
from multiprocessing import Process # 多进程的类
import time
import random
# 创建一个进程类Pros
class Pros(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print('%s 开始运行!' % self.name)
# 随机睡眠1~5秒
time.sleep(random.randrange(1, 5))
print('%s 结束!' % self.name)
if __name__ == "__main__":
# 创建3个子进程
for i in range(3):
num = "p" + str(i+1)
num = Pros(f"子进程{num}")
num.start() # start方法会自动调用进程类中的run方法
4、join函数
在多进程中,join()方法会使主进程进入阻塞,直到调用join()方法的子进程执行完毕。即:使主进程进入阻塞,直到调用join()方法的子进程执行完毕。
5、进程池(Pool)
大家思考一个问题:在一台计算机中进程可以无限制的创建吗?
进程池(Pool)的作用, 当进程数过多时,用于限制进程数。Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,才会创建新的进程来它。
语法:Pool([numprocess [,initializer [, initargs]]]) # 创建进程池
参数介绍:
1 numprocess:要创建的进程数,如果省略,将默认使用cpu_count()的值
2 initializer:是每个工作进程启动时要执行的可调用对象,默认为None
3 initargs:是要传给initializer的参数组
import os, time
from multiprocessing import Process, Pool
def run_a_sub_proc(name):
print(f'子进程:{name}({os.getpid()})开始!')
for i in range(2):
print(f'子进程:{name}({os.getpid()})运行中...')
time.sleep(1)
print(f'子进程:{name}({os.getpid()})结束!')
if __name__ == '__main__':
print(f'主进程({os.getpid()})开始...')
p = Pool(3)
for i in range(1, 5):
p.apply_async(run_a_sub_proc, args=(f"进程-{i}",))
p.close()
p.join()
apply() :该函数用于传递不定参数,主进程会被阻塞直到函数执行结束(不建议使用)函数原型如下: 这是一个阻塞函数。
apply(func, args=(), kwds={})
apply_async :与apply用法一致,但它是非阻塞的且支持结果返回后进行回调,函数原型如下:
apply_async(func[, args=()[, kwds={}[, callback=None]]])
!在采用进程池的时候,要最后手动加process_pool.join()。因为主进程main管理不到进程池,我们需要让主进程join阻塞,等到进程池结束之后,再结束程序。
无论如何 - 现代方法:https://docs.python.org/3/library/concurrent.futures.html
三、进程之间的通信
大家思考一下:在多进程中可以使用全局变量来共享数据吗?
import multiprocessing
import time
# 定义全局变量
g_list = list()
# 添加数据的任务
def add_data():for i in range(5):
g_list.append(i)
print("add:", i)
time.sleep(0.2)
# 代码执行到此,说明数据添加完成
print("add_data:", g_list)
def read_data():
print("read_data", g_list)
if __name__ == '__main__':
# 创建添加数据的子进程
add_data_process = multiprocessing.Process(target=add_data)
# 创建读取数据的子进程
read_data_process = multiprocessing.Process(target=read_data)
# 启动子进程执行对应的任务
add_data_process.start()
# 主进程等待添加数据的子进程执行完成以后程序再继续往下执行,读取数据
add_data_process.join()
read_data_process.start()
print("main:", g_list)
# 总结: 多进程之间不共享全局变量
创建子进程会对主进程资源进行拷贝,也就是说子进程是主进程的一个副本,好比是一对双胞胎,之所以进程之间不共享全局变量,是因为操作的不是同一个进程里面的全局变量,只不过不同进程里面的全局变量名字相同而已。
-> 两个及两个以上使用queue,两个使用pipe
1、进程队列(Queue)通信
Queue([maxsize]):建立一个共享的队列(内部维护着数据的共享),多个进程可以向队列里存/取数据。其中,参数是队列最大项数,省略则无限制。
from multiprocessing import Process, Queue
import time, os
def prodcut(q):
print("开始生产.")
for i in range(5):
time.sleep(1)
q.put('产品'+str(i))
print("产品"+str(i)+"生产完成")
def consume(q):
while True:
prod = q.get()
# get是一个阻塞函数。获取值之后删除。
print("消费者:{},消费产品:{}".format(os.getpid(), prod))
time.sleep(1)
if __name__ == '__main__':
q = Queue()
p = Process(target=prodcut, args=(q, )) # 生产者
c1 = Process(target=consume, args=(q, )) # 消费者1
c2 = Process(target=consume, args=(q, )) # 消费者2
p.start()
c1.start()
c2.start()
p.join() # 当生产者结束后,将两个消费则也结束
c1.terminate()
c2.terminate()
2、管道(Pipe)
如果你创建了很多个子进程,那么其中任何一个子进程都可以对Queue进行存(put)和取(get)。但Pipe不一样,Pipe只提供两个端点,只允许两个子进程进行存(send)和取(recv)。也就是说,Pipe实现了两个子进程之间的通信。
from multiprocessing import Pipe, Pool
import os,time
def product(send_pipe):
print("开始生产.")
for i in range(5):
time.sleep(1)
send_pipe.send("产品"+str(i))
print("产品" + str(i) + "生产完成")
def consume(recv_pipe):
while True:
print("消费者:{},消费产品:{}".format(os.getpid(), recv_pipe.recv()))
time.sleep(1)
if __name__ == '__main__':
# 使用进程池来创建进程
send_pipe, recv_pipe = Pipe()
pool = Pool(2)
pool.apply_async(product, args=(send_pipe,))
pool.apply_async(consume, args=(recv_pipe,))
pool.close()
pool.join()
补充:计算密集型 vs. IO密集型
是否采用多任务的第二个考虑是任务的类型。我们可以把任务分为计算密(CPU)集型和IO密集型。
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、浮点运算、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。-> 多进程
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
协程
🧩 1. 定义
-
协程 (Coroutine) 是一种 用户态的轻量级线程,由程序自己控制切换,而不是操作系统调度。
-
协程运行在 单线程 内,可以在合适的时机 主动让出执行权,切换到别的协程。
👉 所以协程不是“真的并行”,而是一种 更高效的并发。
🧩 2. 和线程的区别
| 特性 | 线程 (Thread) | 协程 (Coroutine) |
|---|---|---|
| 调度者 | 操作系统调度 | 程序自身调度 (用户态) |
| 开销 | 创建、切换需要系统调用,比较重 | 极轻量,一个进程可开成千上万个 |
| 并发模型 | 多个线程抢占 CPU | 单线程内多个协程协作切换 |
| 适合场景 | CPU 密集型、需要并行 | IO 密集型、高并发请求 |
👉 一句话:线程 = 操作系统调度,协程 = 程序自己调度。
🧩 3. Python 里的协程
Python 的协程主要通过 async/await 实现(基于事件循环 asyncio)。
协程的关键是 遇到等待(IO)时主动让出控制权,在等待期间去跑别的任务。
示例:
import asyncio async def task(name): print(f"任务 {name} 开始") await asyncio.sleep(2) # 模拟IO print(f"任务 {name} 完成") async def main(): await asyncio.gather( task("A"), task("B"), task("C") ) asyncio.run(main())
👉 输出:
任务 A 开始 任务 B 开始 任务 C 开始 ... 等 2 秒 ... 任务 A 完成 任务 B 完成 任务 C 完成
虽然是单线程运行,但看起来三个任务像“同时”执行。
🧩 4. 协程的优势
-
比线程更轻量:几万个协程 ≈ 几十个线程的开销。
-
适合 IO 密集型:比如网络爬虫、API 调用、数据库操作。
-
可控性高:你决定什么时候切换,而不是被操作系统随意打断。