AI项目博客
记录 AI 工程实践与大模型应用

Python 学习笔记 31|进程之间不共享变量

6. 进程之间不共享变量

进程之间不共享内存,因此也就不共享任何变量。验证代码如下:

验证代码 1:

# 进程之间不共享内存,因此也就不共享任何变量。
from multiprocessing import Process
num = 100
names = []

def test1():
    global num, names
    num += 10
    names.append('张三')
    print(f'我是 test1 进程,操作之后的num是{num},操作之后的names是{names}')

def test2():
    global num, names
    num -= 10
    names.append('李四')
    print(f'我是 test2 进程,操作之后的num是{num},操作之后的names是{names}')

if __name__ == "__main__":
    print('主进程中的【第一行】代码')
    p1 = Process(target=test1)
    p2 = Process(target=test2)

    p1.start()
    p2.start()

    p1.join()
    p2.join()
    print('主进程中的【最后一行】代码', num, names)

验证代码 2:

# 进程之间不共享内存,因此也就不共享任何变量。
from multiprocessing import Process

def test1(num, names):
    num += 10
    names.append('张三')
    print(f'我是 test1 进程,操作之后的num是{num},操作之后的names是{names}')

def test2(num, names):
    num -= 10
    names.append('李四')
    print(f'我是 test2 进程,操作之后的num是{num},操作之后的names是{names}')

if __name__ == "__main__":
    num = 100
    names = []

    print('主进程中的【第一行】代码')
    p1 = Process(target=test1, args=(num, names))
    p2 = Process(target=test2, args=(num, names))

    p1.start()
    p2.start()

    p1.join()
    p2.join()
    print('主进程中的【最后一行】代码', num, names)

注意:

我们正常编写的变量,在多个进程之间是不共享的,但有些对象是天然被多个进程共享的,比如:我们之前讲过的 Lock 和 RLock,我们后面还会学习到很多天然被多个进程锁共享的对象,例如:multiprocessing.Queue

7. Queue(队列)

  • 我们之前学过 list、tuple、dict,它们的特点是:数据想怎么拿数据,就怎么拿。
  • 队列(Queue)是:一种“先进先出”的数据结构(先放进去的数据,一定会先取出来)。
  • 队列的常见操作如下:
import time
from multiprocessing import Queue, Process

# 创建一个队列(不限制大小,即:不设置容量上限)
q1 = Queue()

# 创建一个队列(最多能保存3个元素)
q2 = Queue(3)

# 1️⃣put方法:往队列里放数据(入队)
q1.put(10)
q1.put(20)
q1.put(30)

# 2️⃣get方法:从队列里取数据(出队)
value1 = q1.get()
value2 = q1.get()
value3 = q1.get()
print(value1)
print(value2)
print(value3)

# 3️⃣empty方法:判断队列是否为空
result = q1.empty()
print(result)

# 4️⃣full方法:判断队列是否已满
q1.put(10)
q1.put(20)
q1.put(30)
result = q1.full()
print(result)

q2.put(100)
q2.put(200)
q2.put(300)
result = q2.full()
print(result)

# 5️⃣qsize方法:获取队列长度
q1.put(10)
q1.put(20)
q1.put(30)
result = q1.qsize()
print(result)

# 6️⃣队列具备等待模式
q2.put(100)
q2.put(200)
q2.put(300)

# (1).当队列已满,继续 put,就会进入等待模式,等别人调用get方法,取走一个元素
q2.put(400)
print('放入完毕')

# (2).当队列已满,执行:put(元素, timeout=秒数),就会等待指定秒数。
q2.put(400, timeout=3)
print('放入完毕')

# (3).put_nowait 方法,会直接向队列中添加元素,不会进入等待模式,若队列已满,会抛出异常。
# 备注:put_nowait 等价于 put(元素, block=False),block的默认值为 True
q2.put_nowait(400)
q2.put(400, block=False)

# get读多了,也会进入等待模式
q2.get()
q2.get()
q2.get()

# (1).当队列已空,继续 get,就会进入等待模式。x
q2.get()

# (2).当队列已空,执行 get(timeout=秒数),就会等待指定秒数。
q2.get(timeout=3)

# (3).get_nowait 方法,会直接读取队列中的元素,不会进入等待模式,若队列已空,会抛出异常
# 备注:get_nowait 等价于 get(block=False)
q2.get_nowait()
q2.get(block=False)

通过多进程演示:当队列满了以后,再次put会等待,当有人从队列中取出元素后,put会继续。

def test(q):
    time.sleep(3)
    result = q.get()
    print('我从队列中取出了一个元素:',result)

# 通过多进程,演示一下:当队列满了以后,再次put会等待,当有人从队列中取出元素后,put会继续。
if __name__ == '__main__':
    # 创建一个队列,让其最多能保存2个元素
    q = Queue(2)
    # put两次,把队列填满
    q.put('')
    q.put('atguigu')
    print(f'队列是否已满:{q.full()}')

    # 创建子进程p1
    p1 = Process(target=test, args=(q, ))
    # 开启子进程p1,让其3秒钟后,从队列中取出一个元素
    p1.start()

    print('即将向已满的队列中添加一个元素........')
    q.put('hello')

    print('目前队列中有的元素是:')
    print(q.get())
    print(q.get())

8. 使用 Queue 实现进程通信

进程之间是不共享数据的,那如果进程之间如何进行数据沟通(进程通信)呢?手段多种多样,我们本小节给大家说通过Queue 实现进程通信。

核心思想: 一个进程负责生产数据,另一个进程负责消费数据,中间通过 Queue 进行“传话”

import time
from multiprocessing import Process, Queue

# 子进程1:往队列里放数据
def test1(q):
    for index in range(5):
        print(f'【test1】放入数据:{index}')
        q.put(index)
        time.sleep(0.5)

# 子进程2:从队列里取数据
def test2(q):
    for index in range(5):
        data = q.get()
        print(f'【test2】取出数据:{data}')
        time.sleep(1)

if __name__ == '__main__':
    q = Queue()

    p1 = Process(target=test1, args=(q,))
    p2 = Process(target=test2, args=(q,))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

备注:q 是在主进程中创建的,但可以被子进程使用,因为 multiprocessing.Queue是跨进程的。

为什么数据不会乱掉? —— 因为队列是先进先出的。

9. 使用 Pipe 实现进程通信

Pipe 就像一根“水管”,一头负责发送,另一头负责接收。

1️⃣ 创建管道

# Pipe() 会返回两个连接对象,它们分别代表管道的两端。
# duplex用于控制管道为单向还是双向,True表示双向,False表示单向
con1, con2 = Pipe(duplex=True)

# 单向 Pipe 的规则:con1只能发送,con2只能接收。

2️⃣发送与接收

  • send方法: 向管道中发送数据。
  • recv方法: 从管道中接收数据。

3️⃣测试代码:

import time
from multiprocessing import Process, Pipe

def test1(con1):
    time.sleep(2)
    con1.send(100)
    print('test1发送了100')

def test2(con2):
    data = con2.recv()
    print(f'test2接收了{data}')

if __name__ == '__main__':
    con1, con2 = Pipe(duplex=False)
    p1 = Process(target=test1, args=(con1,))
    p2 = Process(target=test2, args=(con2,))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

10. 继承 Process 类创建进程

当子进程逻辑比较复杂,或者想把「进程 + 行为」封装成一个整体时,可以使用继承 Process 类的方式去创建进程,这种方式属于“面向对象风格”

1️⃣核心要点:

  • 必须继承 Process类
  • 把子进程要干的事,写进 run() 方法里
  • 依然使用start方法启动进程,不要手动调用 run()!
  • 若子进程不需要参数,可以不写__init__,若需要参数,则需编写__init__
  • 传给的子进程的参数,作为实例属性保存 。

2️⃣示例代码:

from multiprocessing import Process
import os, time

class SpeakProcess(Process):
    def __init__(self, a, b, **kwargs):
        super().__init__(**kwargs)
        self.a = a
        self.b = b

    def run(self):
        for index in range(10):
            print(f'{self.a}--{self.b}--我在说话{index}, 进程pid是:{os.getpid()}, 我的父进程是:{os.getppid()}')
            time.sleep(1)

class StudyProcess(Process):
    def run(self):
        for index in range(15):
            print(f'我在学习{index}, 进程pid是:{os.getpid()}, 我的父进程是:{os.getppid()}')
            time.sleep(1)

if __name__ == '__main__':
    print('我是主进程中的【第一行】打印')
    p1 = SpeakProcess(100, 200)
    p2 = StudyProcess()

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    print('我是主进程中的【最后一行】打印')

11. 进程池(ProcessPoolExecutor)

截至目前,我们已经学会了创建多个进程一起工作,但是我们仍然面临一个问题:如果每来一个任务就创建一个进程,会很浪费系统资源,因为进程创建 / 销毁成本很高,当有大量任务时,系统资源浪费严重。

使用进程池的优势:如何使用进程池统一管理多个子进程,避免频繁创建 / 销毁进程带来的开销,因为进程池会提前创建固定数量的进程,反复使用它们来执行任务。

1️⃣创建『进程池执行器』、使用 submit 方法提交任务、使用 shutdown 方法等待任务完成。

def work(n):
    print(f'work正在执行任务{n}.........{os.getpid()}')
    time.sleep(1)

if __name__ == '__main__':
    print('---------start-------------')
    # 创建一个进程池执行器
    executor = ProcessPoolExecutor(3)
    # 使用 submit 方法提交任务(submit 只负责“提交任务”,不会阻塞主进程)
    executor.submit(work, 1)
    executor.submit(work, 2)
    executor.submit(work, 3)
    executor.submit(work, 4)
    executor.submit(work, 5)
    executor.submit(work, 6)
    executor.submit(work, 7)
    # shutdown 的作用:不再接收新的任务。
    # wait=True 的作用:阻塞主进程,等待进程池中所有任务执行完毕。
    executor.shutdown(wait=True)
    print('---------end-------------')

2️⃣获取子进程执行后的返回结果(Future类的实例对象 + result方法)

def work(n):
    print(f'work正在执行任务{n}.........{os.getpid()}')
    time.sleep(1)
    return f'我是任务{n}的结果'

if __name__ == '__main__':
    print('---------start-------------')
    # 创建一个进程池执行器
    executor = ProcessPoolExecutor(3)
    # 使用 submit 方法提交任务(submit 只负责“提交任务”,不会阻塞主进程)
    # f1 = executor.submit(work, 1)
    # f2 = executor.submit(work, 2)
    # f3 = executor.submit(work, 3)
    # f4 = executor.submit(work, 4)
    # f5 = executor.submit(work, 5)
    # f6 = executor.submit(work, 6)
    # f7 = executor.submit(work, 7)
    futures = [executor.submit(work, index) for index in range(1, 8)]
    # 阻塞主进程,等待进程池中所有任务执行完毕。
    executor.shutdown(wait=True)
    # print(f1.result())
    # print(f2.result())
    # print(f3.result())
    # print(f4.result())
    # print(f5.result())
    # print(f6.result())
    # print(f7.result())
    for f in futures:
        print(f.result())
    print('---------end-------------')

3️⃣使用 as_completed:按“完成顺序”获取结果

def work(n):
    print(f'work正在执行任务{n}.........{os.getpid()}')
    if n == 1:
        time.sleep(15)
    elif n == 2:
        time.sleep(10)
    else:
        time.sleep(1)
    return f'我是任务{n}的结果'

if __name__ == '__main__':
    print('---------start-------------')
    # 创建一个进程池执行器
    executor = ProcessPoolExecutor(3)
    # 使用 submit 方法提交任务(submit 只负责“提交任务”,不会阻塞主进程)
    futures = [executor.submit(work, index) for index in range(1, 8)]
    # 准备一个 result_list 去收集任务的具体结果
    result_list = []
    # 收集每个任务的结果
    for f in as_completed(futures):
        result_list.append(f.result())
    # 阻塞主进程,等待进程池中所有任务执行完毕。
    executor.shutdown(wait=True)
    # 打印最终的结
    print(result_list)
    print('---------end-------------')

4️⃣使用 add_done_callback 方法,为任务添加完成时的回调函数。

def work(n):
    print(f'work正在执行任务{n}.........{os.getpid()}')
    if n == 1:
        time.sleep(15)
    elif n == 2:
        time.sleep(10)
    else:
        time.sleep(1)
    return f'我是任务{n}的结果'

if __name__ == '__main__':
    print('---------start-------------')
    # 创建一个进程池执行器
    executor = ProcessPoolExecutor(3)
    # 准备一个 result_list 列表去收集任务的结果
    result_list = []
    # 任务完成后的回调函数
    def done_func(futrue):
        result_list.append(futrue.result())
    # 开启7个任务,并指定回调函数
    for index in range(1, 8):
        f = executor.submit(work, index)
        f.add_done_callback(done_func)
    # 等所有任务都完成
    executor.shutdown(wait=True)
    # 打印最终的结果(按“完成的顺序”获取)
    print(result_list)
    print('---------end-------------')

5️⃣️使用 map 方法批量提交任务(注意:map方法本身不阻塞,但读取其返回的生成器对象是阻塞的,并且得到结果的顺序,与任务分配的顺序是一致的)

map方法会把这一批任务提交到进程池里执行,它会立刻返回一个生成器,真正的阻塞发生在:生成器取结果时,如 list(result)

def work(n):
    print(f'work正在执行任务{n}.........{os.getpid()}')
    if n == 1:
        time.sleep(15)
    elif n == 2:
        time.sleep(10)
    else:
        time.sleep(1)
    return f'我是任务{n}的结果'

if __name__ == '__main__':
    print('---------start-------------')
    # 创建一个进程池执行器
    executor = ProcessPoolExecutor(3)
    # 通过 map 方法批量提交任务(结果按照提交的顺序来)
    results = executor.map(work, [1, 2, 3, 4, 5, 6, 7])
    # 获取 results 生成器中的内容
    print(list(results))
    # 等所有任务都完成
    executor.shutdown(wait=True)
    print('---------end-------------')

6️⃣使用 with:进程池的“自动回收”写法,离开 with 代码块时自动执行 shutdown(wait=True)

def work(n):
    print(f'work正在执行任务{n}.........{os.getpid()}')
    if n == 1:
        time.sleep(15)
    elif n == 2:
        time.sleep(10)
    else:
        time.sleep(1)
    return f'我是任务{n}的结果'

if __name__ == '__main__':
    print('---------start-------------')
    # 创建一个进程池执行器
    with ProcessPoolExecutor(3) as executor:
        # 通过 map 方法批量提交任务(结果按照提交的顺序来)
        results = executor.map(work, [1, 2, 3, 4, 5, 6, 7])
        # 获取 results 生成器中的内容
        print(list(results))
    print('---------end-------------')
赞(0)
未经允许不得转载:AI项目博客 » Python 学习笔记 31|进程之间不共享变量

评论 抢沙发

AI项目博客

专注 AI 工程化落地 —— 覆盖智能体、大模型、Java AI 与 Python 的学习笔记,以及 AI 平台的项目实战记录。

关于本站文章归档

登录

找回密码

注册