Python 多进程

544 阅读7分钟

「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战」。

进程

在我们使用电脑时,会同时运行很多个程序,比如在我运行浏览器的同时还可以运行 QQ。我们可以通过右击任务栏的方式打开任务管理器,如下图:

在这里插入图片描述

我们每个程序就可以看成一个或者多个进程。我们可以简单理解为一个程序就是一个进程。我们今天的内容就是利用 Python 来创建,并开启多个进程。

进程的开启

开启一个进程

Python 操作进程的类都定义在 multiprocessing 模块,其中最主要的类就是我们的进程类 Process,下面是我们创建一个进程的例子:

# 导入进程的模块
from multiprocessing import Process

# 定义一个函数
def func():
    print("进程运行了")

if __name__ == '__main__':
    # 创建一个进程,把 target 参数指定为 func
    p1 = Process(target=func)
    # 开启进程
    p1.start()

首先需要导入我们的模块,如何创建 Process 类的对象,默认情况下我们需要传入一个 target 参数。traget 接收一个函数,就是我们开启进程时会执行的函数。

在创建进程后,我们需要调用 start 方法开启我们的进程。这个时候进程才真正运行了。

开启多个进程

当然,我们还可以开启多个进程:

from multiprocessing import Process

def func():
    print("进程运行了")

if __name__ == '__main__':
    p1 = Process(target=func)
    p2 = Process(target=func)
    p1.start()
    p2.start()

在代码中我们创建了两个进程,并分别开启进程。运行结果如下:

进程运行了
进程运行了

可以看到,两个进程都执行了函数内容。但是你可能会好奇,if__name__=="__main__": 有什么用,如果你平时写代码不写这一段代码没什么关系。但是当你使用到多进程时,如果少了这一句代码就会报错,比如下面这段代码:

from multiprocessing import Process

def func():
    print("进程运行了")

p1 = Process(target=func)
p2 = Process(target=func)
p1.start()
p2.start()

运行时会报错,原因是 Python 实现多进程的操作。进程之间是数据隔离的,我们两个不同的进程不可能访问同一个函数。为了能让两个进程执行一个函数,在运行多个进程时,Python 会把我们写的 py 文件复制一份然后运行。当我们把创建进程和开启进程的操作写在 main 外面,就会出现无限复制文件的问题。而加了 main 后,则只会在当前运行的文件中创建进程。

给函数传入参数

我们看下面的代码:

from multiprocessing import Process

def func(x):
    print(x , "进程运行了")

if __name__ == '__main__':
    p1 = Process(target=func, args=(1, ))
    p1.start()

我们给 Process 传入了一个 args 参数,参数的内容是一个元组,但是我只有一个元素。这个时候需要记住必须要加一个逗号,我们执行程序,运行结果如下:

1 进程运行了

可以看到参数正确穿过去了。

join 方法

join 的基本使用

join 方法的作用是等待进程结束,我们在代码中看看效果:

import time
from multiprocessing import Process

def func(x):
    for i in range(10):
        time.sleep(0.5)
        print(x, i)

if __name__ == '__main__':
    p1 = Process(target=func, args=("zack", ))
    p2 = Process(target=func, args=("rudy", ))
    p1.start()
    p1.join()
    p2.start()

在 func 中,我们接收一个 x 参数用于区分进程。在函数内部我们循环执行 sleep 并输出内容。下面是运行效果:

zack 0
zack 1
zack 2
zack 3
zack 4
zack 5
zack 6
zack 7
zack 8
zack 9
rudy 0
rudy 1
rudy 2
rudy 3
rudy 4
rudy 5
rudy 6
rudy 7
rudy 8
rudy 9

Process finished with exit code 0

可以看到 p2 在 p1 完全执行完后才开始执行。这是因为我们在 p2 执行开始前调用了 p1.join,而 join 后的代码都会等 p1 执行完后才执行。

等待所有子进程执行完

我们在 py 文件中通过 Process 创建的是我们 Python 主程序的子进程,如果我们想要在所有进程执行完后执行一段代码应该怎样呢?我们先来尝试下面的代码:

import time
from multiprocessing import Process

def func(x):
    for i in range(5):
        time.sleep(0.5)
        print(x, i)

if __name__ == '__main__':
    p1 = Process(target=func, args=("zack", ))
    p2 = Process(target=func, args=("rudy", ))
    p1.start()
    p2.start()
    print("所有进程都执行完了")

我们在开启进程后输出一条语句,但是实际情况却和我们想的不一样,结果如下:

所有进程都执行完了
zack 0rudy
 0
rudyzack 1
 1
zack 2rudy 2

zack 3rudy 3

rudyzack  44

因为开启进程需要花点时间,所有输出语句还在最前面执行了。我们可以尝试使用 join 方法,但是如果我们依次调用 join 方法的话没有达到多进程的效果,所以我们需要按照下面的方式写:

import time
from multiprocessing import Process

def func(x):
    for i in range(5):
        time.sleep(0.5)
        print(x, i)

if __name__ == '__main__':
    pl = []

    for i in range(5):
        p = Process(target=func, args=(str(i)+"zack", ))
        pl.append(p)
        p.start()
    for p in pl:
        p.join()
    print("所有进程都执行完了")

我们使用了一个列表把所有进程都装了进去,然后在所有进程开启后再依次 join,这样我们就可以在实现多进程的同时还能等待所有进程执行完后再执行一些操作。下面是运行效果:

0zack 0
2zack4zack 0
 0
3zack 01zack
 0
0zack 1
2zack 1
4zack 1
1zack 3zack 11

0zack 2
2zack 2
4zack 2
1zack 23zack
 2
0zack 3
4zack 2zack 33

1zack 3
3zack 3
0zack 4
2zack4zack  44

1zack 4
3zack 4
所有进程都执行完了

使用面向对象的方式开启进程

创建自己的进程类

除了创建 Process 类外,我们还可以继承 Process 来实现多进程,操作和之前区别不大,我们下定义一个类,进程 Process:

from multiprocessing import Process


class MyProcess(Process):

    def run(self) -> None:
        print("执行了进程")

if __name__ == '__main__':
    p1 = MyProcess()
    p1.start()

我们创建了一个 MyProcess 类,然后继承了 Process 类,并实现了 run 方法。然后我们运行一下程序:

执行了进程

可以看到我们执行了 run 方法中的内容。其实我们调用 start 执行的就是 run 方法的内容。

扩展进程的参数

我们还可以在自定义的进程类中扩展一些参数具体代码如下:

from multiprocessing import Process


class MyProcess(Process):

    def __init__(self, name):
        # 初始化 Process 的参数
        super().__init__()
        self.name = name

    def run(self) -> None:
        print(self.name, "执行了进程")


if __name__ == '__main__':
    p1 = MyProcess("zack")
    p1.start()

我们实现了 init 方法,然后调用父类的 init 方法初始化 Process 类的参数。然后我们在自己的 init 方法中,传入了一个 name 参数。当然我们为了更通用,可以再改造一下:

from multiprocessing import Process


class MyProcess(Process):

    def __init__(self, name, *args, **kwargs):
        # 初始化 Process 的参数
        super().__init__(*args, **kwargs)
        self.name = name

    def run(self) -> None:
        print(self.name, "执行了进程")


if __name__ == '__main__':
    p1 = MyProcess("zack")
    p1.start()

这样我们可以传入自己添加的参数,同时也能传入 Process 自己的参数。

进程锁

在进程之前是数据隔离的,那为什么我们还要锁呢?进程的数据隔离实际上指的是内存隔离,两个进程之间不能直接进行数据交流,但是我们可以通过文件或者网络来进行通信。而在这个时候,就可能出现数据不安全的问题,所以我们需要学习进程锁。

from multiprocessing import Process


def func():
    with open("0.txt") as f:
        num = int(f.read())
    num += 1
    with open("0.txt", "w") as f:
        f.write(str(num))


if __name__ == '__main__':
    for i in range(100):
        p = Process(target=func)
        p.start()

我们创建了一个 0.txt 文件,内容如下:

1

我们运行后可能会报错。我们先忽略错误,查看一下运行后文件的内容:

97

我们在程序中加了 100 次 1,但是实际上却没有这么多次。这就是数据不安全的,因此我们需要锁。具体操作如下:

from multiprocessing import Lock
from multiprocessing import Process


def func(lock):
    lock.acquire()
    with open("0.txt") as f:
        num = int(f.read())
    num += 1
    with open("0.txt", "w") as f:
        f.write(str(num))
    lock.release()


if __name__ == '__main__':
    lock = Lock()
    for i in range(100):
        p = Process(target=func, args=(lock, ))
        p.start()

我们创建了一个 Lock 类,如何将 lock 作为参数传入函数,如何在可能出现数据不安全的地方 lock.acquire(),如何结束时释放锁 lock.release(),我们再运行一下,会发现结果正确了。但是数据安全效率会有一定的削减,因为我们调用 lock 时会有等待的过程。

以上就是多进程的内容了。在了解多进程后理解线程的理解就更容易了,感兴趣的读者可以去看看 threading 模块。