python中如何优雅地使用多进程(1)

众所周知,python中存在GIL锁,导致同一时间只能有一个线程在CPU上运行,而且是单个CPU上运行,不管你的CPU有多少核数。然而如今大多数的个人电脑或者服务器都是多核CPU,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。

1.如何理解进程?

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

2.Process类

在python中常用到的多进程相关的包有 multiprocessingconcurrent,归根结底上都是使用到了Process类,只是后者简化了操作,本文只介绍multiprocessing,对于concurrent会在之后进行详细介绍。

磨刀不费砍材功,我们先了解Process类的相关用法,然后在了解多进程的使用。

语法:Process([group [, target [, name [, args [, kwargs]]]]])

Process参数:

参数释义
group参数未使用,默认值为None。
target表示调用对象,即子进程要执行的任务
args表示调用的位置参数元组
kwargs表示调用对象的字典。如kwargs = {‘name’:Jack, ‘age’:18}。
name子进程名称。

Process属性方法:

方法/属性说明
start()启动进程,调用进程中的run()方法。
run()进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 。调用run()函数的不可以调用join()
terminate()强制终止进程,不会进行任何清理操作。如果该进程终止前,创建了子进程,那么该子进程在其强制结束后变为僵尸进程;如果该进程还保存了一个锁那么也将不会被释放,进而导致死锁。使用时,要注意。is_alive() 判断某进程是否存活,存活返回True,否则False。
is_alive()判断某进程是否存活,存活返回True,否则False
join([timeout])主线程等待子线程终止。timeout为可选择超时时间;需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 。
daemon默认值为False,如果设置为True,代表该进程为后台守护进程;当该进程的父进程终止时,该进程也随之终止;并且设置为True后,该进程不能创建子进程,设置该属性必须在start()之前
name进程名称。
exitcode进程运行时为None,如果为-N,表示被信号N结束了。
authkey进程身份验证,默认是由os.urandom()随机生成32字符的字符串。这个键的用途是设计涉及网络连接的底层进程间的通信提供安全性,这类连接只有在具有相同身份验证才能成功。

3.如何创建进程?

Python 创建的子进程执行的内容,和启动该进程的方式有关。而根据不同的平台,启动进程的方式大致可分为以下 3 种:

 multiprocessing.set_start_method('fork')
 # python 启动进程方式·
 # spawn  windows or mac os
 # fork,forkserver  linux 
名称释义
spawnwindow和MacOS下默认使用该方式,是最常见的方式。父进程会启动一个新的 Python 解释器,该解释器只会从父进程继承必要的资源来支持运行进程对象的run方法。值得注意的是,spwan产生的进程不会复制父进程中不必要的文件标识符或者句柄,这是相对于fork的一个优势。但是代价是spawn产生子进程的速度比较慢。
fork只适用于Unix,创建的子进程会默认复制一份主进程的数据。双方数据互相不影响
forserver是一个独立的进程,此后需要产生子进程的时候,父进程需要联系该进程 fork 一个子进程。因为 forkserver 本身是一个单线程进程,所以是线程安全的。而且,与 spawn 类似,子进程只会继承必要的资源。

方法

​ 此方法是直接使用multiprocessing。

​ 将要执行的操作直接封装到方法中,在创建Process的时候,传入函数名称以及相关参数。

import time
from multiprocessing import Process

# 无参数
def process_without_args()->None:
    print("process_without_args")
    time.sleep(3)

# 有参数
def process_with_args(name:str)->None:
    print("process_with_args age is", name)
    time.sleep(3)


if __name__ == '__main__':
     # 查看自己的CPU核心数
    print(multiprocessing.cpu_count())
    
    processes = []
    for i in range(3):

        # 无参数
        without_arg = Process(target=process_without_args)

        # 有参数,args传入元组
        with_arg = Process(target=process_with_args, args=(f"process {str(i)}",))
        without_arg.start()
        with_arg.start()

        processes.append(with_arg)

    #  join 主线程等待子线程,
    #  如果主线程等待子线程结束,end将最后输出
    #  反之 end不会再最后输出
    [with_arg.join() for with_arg in processes]
    print("end ")

    #结果
    #process_with_args age is process 1
    # process_with_args age is process 2
    # process_without_args
    # process_without_args
    # process_without_args
    # process_with_args age is process 0
    # end 

方法二

​ 此方法是直接使用multiprocessing。

​ 通过继承Process类的方式,需要重写其中的run方法。

from multiprocessing import Process


class MyProcess(Process):
    def __init__(self, name: str, i: int):
        super().__init__()
        self.name = name
        self.i = i

    def run(self):
            print(f"name {self.name} i {self.i}")


if __name__ == '__main__':
    p1 = MyProcess('张三', 3)
    p2 = MyProcess('李四', 4)
    p3 = MyProcess('王五', 5)

    p1.start()
    # 自动调用run()
    p2.start()
    p3.run()  # 直接调用run()

    p2.join()
    p1.join()
    # p3.join()  # 调用run()函数的不可以调用join()

    print("主进程结束")
    #name 王五 i 5
    # name 张三 i 3
    # name 李四 i 4
    # 主进程结束

4.进程间通信与锁

进程是系统独立调度核分配系统资源(CPU、内存)的基本单位,进程之间是相互独立的。

每启动一个新的进程相当于把数据进行了一次克隆,子进程里的数据修改无法影响到主进程中的数据,不同子进程之间的数据也不能共享,这是多进程在使用中与多线程最明显的区别。

但是难道Python多进程中间难道就是孤立的吗?当然不是,python也提供了多种方法实现了多进程中间的通信和数据共享(可以修改一份数据)

Python提供了不少进程间通信的方式,包含管道,队列,数据共享,共享内存等方式。

下文将一一解释。

队列

创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

Queue[maxsize]:maxsize是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-olwVDOrK-1636926935464)(file:///C:\Users\小佰\AppData\Roaming\Tencent\Users\707979910\TIM\WinTemp\RichOle\ELGLRDOA(D(115B)]2`S17IP.png)

import os
import time
import multiprocessing


def input_queue(q):
    info = f"放入数据 {str(time.asctime())}"
    q.put(info)
    print(info)

def output_queue(q):
    info = q.get()
    print(f'从队列中取数据 {info}')

if __name__ == '__main__':
    multiprocessing.freeze_support()
    record_one = []
    record_two = []
    queue = multiprocessing.Queue(3)

    # 放入数据
    for i in range(10):
        p = multiprocessing.Process(target=input_queue, args=(queue,))
        p.start()
        record_one.append(p)

    # 取出数据
    for i in range(10):
        p = multiprocessing.Process(target=output_queue, args=(queue,))
        p.start()
        record_one.append(p)

    for p in record_one:
        p.join()

    for p in record_two:
        p.join()

管道

Pipe([duplex]):在线程之间创建一条管道,并返回元祖(con1,con2),其中con1,con2表示管道两端连接的对象。

duplex:默认管道为全双工的,如果将duplex映射为False,con1只能用于接收,con2只能由于发送。

应该特别注意管道端点的正确管理问题,如果是生产者或消费者中都没有使用管道的某个端点,就应将它关闭。这也说明了为何在生产者中关闭了管道的输出端,在消费者中关闭管道的输入端。如果忘记执行这些步骤,程序可能在消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生成EOFError异常。因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。

pipe的相关方法,如图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mciYKpfY-1636926935468)(C:\Users\小佰\AppData\Roaming\Typora\typora-user-images\image-20211114160715289.png)]

from multiprocessing import Process, Pipe


def func(conn):
    conn.send({"1":2})
    conn.send("路在脚下!")  # 发送
    conn.close()


if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=func, args=(child_conn,))
    p.start()
    print(parent_conn.recv())  # 接收
    print(parent_conn.recv())  # 接收
    p.join()

Manager

​ 队列和管道可以认为是两个进程之间的通信。使用manager,则是数据共享。manager支持python的基本数据类型, 例如dict 和 list,通过manager来进行创建,使用方式与普通的一致。manager是进程安全的。

from multiprocessing import Process, Manager


def fun1(dic, lis, index):
    dic[index] = 'a'
    lis.append(index)  


if __name__ == '__main__':
    with Manager() as manager:
        dic = manager.dict()  # 注意字典的声明方式,不能直接通过{}来定义
        l = manager.list([])  # []

        process_list = []
        for i in range(10):
            p = Process(target=fun1, args=(dic, l, i))
            p.start()
            process_list.append(p)

        for res in process_list:
            res.join()
        print(dic)
        print(l)

因为多个进程同时访问一个数据,与多线程一样,会产生数据竞争,在使用的时候也可以加锁,来保证进程的安全性。

# 使用进程锁
def task(lock):
    lock.acquire()
    do something
    lock.release()
if __name__ == '__main__':
    lock = Lock()
    for i in range(10):
        p = Process(target=task, args=(lock,))
        p.start()

SharedMemory

共享内存,python3.8新功能。

官方网址:https://docs.python.org/zh-cn/3/library/multiprocessing.shared_memory.html

共享内存是指 “System V 类型” 的共享内存块(虽然可能和它实现方式不完全一致)而不是 “分布式共享内存”。这种类型的的共享内存允许不同进程读写一片公共(或者共享)的易失性存储区域。一般来说,进程被限制只能访问属于自己进程空间的内存,但是共享内存允许跨进程共享数据,从而避免通过进程间发送消息的形式传递数据。相比通过磁盘、套接字或者其他要求序列化、反序列化和复制数据的共享形式,直接通过内存共享数据拥有更出色性能。

本文只是简述ShareableList用法。提供一个可修改的类 list 对象,其中所有值都存放在共享内存块中。这限制了可被存储在其中的值只能是 int, float, bool, str (每条数据小于10M), bytes (每条数据小于10M)以及 None 这些内置类型。它另一个显著区别于内置 list 类型的地方在于它的长度无法修改(比如,没有 append, insert 等操作)且不支持通过切片操作动态创建新的 ShareableList 实例。

class multiprocessing.shared_memory.ShareableList(sequence=None, *, name=None)
提供一个可修改的类 list 对象,其中所有值都存放在共享内存块中。这限制了可被存储在其中的值只能是 int, float, bool, str (每条数据小于10M), bytes (每条数据小于10M)以及 None 这些内置类型。它另一个显著区别于内置 list 类型的地方在于它的长度无法修改(比如,没有 append, insert 等操作)且不支持通过切片操作动态创建新的 ShareableList 实例。

sequence 会被用来为一个新的 ShareableList 填充值。 设为 None 则会基于唯一的共享内存名称关联到已经存在的 ShareableList。

name 是所请求的共享内存的唯一名称,与 SharedMemory 的定义中所描述的一致。 当关联到现有的 ShareableList 时,则指明其共享内存块的唯一名称并将 sequence 设为 None。

count(value)
返回 value 出现的次数。

index(value)
返回 value 首次出现的位置,如果 value 不存在, 则抛出 ValueError 异常。

format
包含由所有当前存储值所使用的 struct 打包格式的只读属性。

shm
存储了值的 SharedMemory 实例。

示例代码:

# 生成share memory list
import time
from multiprocessing import shared_memory
shm_a = shared_memory.ShareableList(['张三', 2, 'abc'], name='123')
time.sleep(20)
print(shm_a)
shm_a.shm.close()
shm_a.shm.unlink()

# 获取并修改
import time
from multiprocessing import shared_memory

shm_a = shared_memory.ShareableList(name='123')
print(shm_a)
shm_a[0] = "9999"
print(shm_a)
shm_a.shm.close()
shm_a.shm.unlink()

如果您觉得这篇文章对您有帮助,欢迎关注微信公众号:码上小佰,笔者会分享更多干货的文章。

参考:

​ https://blog.csdn.net/qq_33567641/article/details/81947832

​ https://zhuanlan.zhihu.com/p/64702600

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值