python3多进程与进程池

使用进程并发主要依赖于Python的multiprocessing 和 mpi4py 的两个模块。

1.多进程

multiprocessing主要包括如下方法和属性:

方法介绍:

  •  p.start():启动进程,并调用该子进程中的p.run() 
  • p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
  • p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,(使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁 )
  • p.is_alive():如果p仍然运行,返回True 
  • p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。 
  • timeout是可选的超时时间,需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程

属性介绍

  •  p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置守护进程:跟随着父进程的代码执行结束,守护进程就结束
  • p.name:进程的名称
  • p.pid:进程的pid
  • p.exitcode:进程在运行时为None、如果为–N,表示被信号N结束(了解即可)
  • p.authkey:进程的身份验证键,默认是由os.urandom()随机生成的32字符的字符串。这个键的用途是为涉及网络连接的底层进程间通信提供安全性,这类连接只有在具有相同的身份验证键时才能成功

1.创建一个进程

Python中的multiprocessing库创建进程的步骤如下:

  1. 创建进程对象
  2. 调用 start() 方法,开启进程的活动
  3. 调用 join() 方法,在进程结束之前主进程一直等待

示例

import multiprocessing

def foo(i):
    print ('called function in process: %s' %i)
    return

if __name__ == '__main__':
    Process_jobs = []
    for i in range(5):
        p = multiprocessing.Process(target=foo, args=(i,))
        Process_jobs.append(p)
        p.start()
        p.join()

结果如下:

called function in process: 0
called function in process: 1
called function in process: 2
called function in process: 3
called function in process: 4

进程对象创建时需要分配一个函数,作为进程的执行任务。如示例中的foo(),可以使用元组的形式给函数传递一些参数

上面的代码中,主进程是指运行整个脚本的进程,也就是执行if __name__ == '__main__':之后的代码的进程。主进程负责创建和管理子进程

2.给进程起个名字

前面创建了一个进程,分配目标函数和函数变量。给进程分配一个名字,有助于debug。

命名进程需要为进程对象提供 name 参数

进程的默认名字是Process-N这种方式

代码如下

# 命名一个进程
import multiprocessing
import time

def foo():
    name = multiprocessing.current_process().name
    print("Starting %s \n" % name)
    #time.sleep(3)
    print("Exiting %s \n" % name)

if __name__ == '__main__':
    process_with_name = multiprocessing.Process(name='foo_process', target=foo)
    process_with_name.daemon = True  # 注意原代码有这一行,但是译者发现删掉这一行才能得到正确输出
    process_with_default_name = multiprocessing.Process(target=foo)
    process_with_name.start()
    process_with_default_name.start()

》》》
开始运行...

Starting foo_process 

Exiting foo_process 

Starting Process-2 

Exiting Process-2 


运行结束。

3.后台运行进程

在处理较大任务时,可以将 进程作为后台进程,multiprocessing模块提供了后台进程选项

可以使用daemon选项设置进程后台运行。

代码如下

# 命名一个进程
import multiprocessing
import time

def foo():
    name = multiprocessing.current_process().name
    print("Starting %s \n" % name)
    time.sleep(3)
    print("Exiting %s \n" % name)

if __name__ == '__main__':
    process_with_name = multiprocessing.Process(name='foo_process', target=foo)
    process_with_name.daemon = True  # foo_process进程将会后台运行
    process_with_default_name = multiprocessing.Process(target=foo)
    process_with_name.start()
    process_with_default_name.start()

代码的输出如下:

开始运行...

Starting foo_process 

Starting Process-2 

Exiting Process-2 


运行结束。

 这段代码中if __name__ == '__main__'主进程并没有等待子进程执行完成。使用join才会等待。若要main主进程等待子进程执行完成再退出,则添加如下两行代码

    process_with_name.join()
    process_with_default_name.join()

4.杀死进程

可以使用 terminate() 方法立即杀死一个进程,可以使用 is_alive() 方法来判断一个进程是否还存活

代码如下

# 杀死一个进程
import multiprocessing
import time

def foo():
        print('Starting function')
        time.sleep(0.1)
        print('Finished function')

if __name__ == '__main__':
        p = multiprocessing.Process(target=foo)
        print('Process before execution:', p, p.is_alive())
        p.start()
        print('Process running:', p, p.is_alive())
        p.terminate()
        print('Process terminated:', p, p.is_alive())
        p.join()
        print('Process joined:', p, p.is_alive())
        print('Process exit code:', p.exitcode)

输出为

Process before execution: <Process name='Process-1' parent=11242 initial> False
Process running: <Process name='Process-1' pid=11244 parent=11242 started> True
Process terminated: <Process name='Process-1' pid=11244 parent=11242 started> True
Process joined: <Process name='Process-1' pid=11244 parent=11242 stopped exitcode=-SIGTERM> False
Process exit code: -15

上面的代码用 is_alive() 方法监控进程生命周期。然后通过调用 terminate() 方法结束进程。

通过读进程的 ExitCode 状态码(status code)验证进程已经结束, ExitCode 可能的值如下:

  • == 0: 没有错误正常退出
  • > 0: 进程有错误,并以此状态码退出
  • < 0: 进程被 -1 * 的信号杀死并以此作为 ExitCode 退出

输出的 ExitCode 是 -15 。负数表示子进程被数字为15的信号杀死。

5.子类中使用进程

实现一个自定义的进程子类,需要以下三步:

  • 定义 Process 的子类
  • 覆盖 __init__(self [,args]) 方法来添加额外的参数
  • 覆盖 run(self, [.args]) 方法来实现 Process 启动的时候执行的任务

创建 Porcess 子类之后,你可以创建它的实例并通过 start() 方法启动它,启动之后会运行 run() 方法。

# -*- coding: utf-8 -*-
# 自定义子类进程
import multiprocessing

class MyProcess(multiprocessing.Process):
        def run(self):
                print ('called run method in process: %s' % self.name)
                return

if __name__ == '__main__':
        for i in range(5):
                p = MyProcess()
                p.start()
                p.join()

输出为

called run method in process: MyProcess-1
called run method in process: MyProcess-2
called run method in process: MyProcess-3
called run method in process: MyProcess-4
called run method in process: MyProcess-5

由于没有指定进程名字,且是自定义了类,故进程的名字默认是MyProcess-N的方式

6.在进程间交换对象

Multiprocessing有两种方式可以交换对象:队列和管道

1.使用队列交换对象

可以通过队列数据结构来共享对象。Queue 返回一个进程共享的队列,是线程安全的,也是进程安全的。任何可序列化的对象(Python通过 pickable 模块序列化对象)都可以通过它进行交换。

使用队列来实现生产者-消费者问题。 Producer 类生产item放到队列中,然后 Consumer 类从队列中移除它们。代码如下:

import multiprocessing
import random
import time

class Producer(multiprocessing.Process):
    def __init__(self, queue):
        multiprocessing.Process.__init__(self)
        self.queue = queue

    def run(self):
        for i in range(10):
            item = random.randint(0, 256)
            self.queue.put(item)
            print("Process Producer : item %d appended to queue %s" % (item, self.name))
            time.sleep(1)
            print("The size of queue is %s" % self.queue.qsize())

class Consumer(multiprocessing.Process):
    def __init__(self, queue):
        multiprocessing.Process.__init__(self)
        self.queue = queue

    def run(self):
        while True:
            if self.queue.empty():
                print("the queue is empty")
                break
            else:
                time.sleep(2)
                item = self.queue.get()
                print('Process Consumer : item %d popped from by %s \n' % (item, self.name))
                time.sleep(1)

if __name__ == '__main__':
    queue = multiprocessing.Queue()
    process_producer = Producer(queue)
    process_consumer = Consumer(queue)
    process_producer.start()
    process_consumer.start()
    process_producer.join()
    process_consumer.join()

其中一次运行可能的输出为

the queue is empty
Process Producer : item 59 appended to queue Producer-1
The size of queue is 1
Process Producer : item 41 appended to queue Producer-1
The size of queue is 2
Process Producer : item 184 appended to queue Producer-1
The size of queue is 3
Process Producer : item 158 appended to queue Producer-1
The size of queue is 4
Process Producer : item 114 appended to queue Producer-1
The size of queue is 5
Process Producer : item 118 appended to queue Producer-1
The size of queue is 6
Process Producer : item 157 appended to queue Producer-1
The size of queue is 7
Process Producer : item 201 appended to queue Producer-1
The size of queue is 8
Process Producer : item 99 appended to queue Producer-1
The size of queue is 9
Process Producer : item 81 appended to queue Producer-1
The size of queue is 10

如果在macos系统上会出现错误,  File "/usr/local/Cellar/python@3.9/3.9.5/Frameworks/Python.framework/Versions/3.9/lib/python3.9/multiprocessing/queues.py", line 126, in qsize
    return self._maxsize - self._sem._semlock._get_value()
NotImplementedError
Process Consumer : item 0 popped from by Consumer-2 

解决参考Queue.qsize() 可能会在未实现 sem_getvalue() 的 Unix 平台上引发 NotImplementedError 异常的解决办法

队列还有一个 JoinableQueue 子类,它有以下两个额外的方法:

  • task_done(): 此方法意味着之前入队的一个任务已经完成,比如, get() 方法从队列取回item之后调用。所以此方法只能被队列的消费者调用。
  • join(): 此方法将进程阻塞,直到队列中的item全部被取出并执行。

( Microndgt 注:因为使用队列进行通信是一个单向的,不确定的过程,所以你不知道什么时候队列的元素被取出来了,所以使用task_done来表示队列里的一个任务已经完成。

这个方法一般和join一起使用,当队列的所有任务都处理之后,也就是说put到队列的每个任务都调用了task_done方法后,join才会完成阻塞。)

2.使用管道交换对象

一个管道可以做以下事情:

  • 返回一对被管道连接的连接对象
  • 然后对象就有了 send/receive 方法可以在进程之间通信

下面是管道用法的一个简单示例。这里有一个进程管道从0到9发出数字,另一个进程接收数字并进行平方计算。

import multiprocessing

def create_items(pipe):
    output_pipe, _ = pipe
    for item in range(10):
        output_pipe.send(item)
    output_pipe.close()

def multiply_items(pipe_1, pipe_2):
    close, input_pipe = pipe_1
    close.close()
    output_pipe, _ = pipe_2
    try:
        while True:
            item = input_pipe.recv()
            output_pipe.send(item * item)
    except EOFError:
        output_pipe.close()

if __name__== '__main__':
    # 第一个进程管道发出数字
    pipe_1 = multiprocessing.Pipe(True)
    process_pipe_1 = multiprocessing.Process(target=create_items, args=(pipe_1,))
    process_pipe_1.start()
    # 第二个进程管道接收数字并计算
    pipe_2 = multiprocessing.Pipe(True)
    process_pipe_2 = multiprocessing.Process(target=multiply_items, args=(pipe_1, pipe_2,))
    process_pipe_2.start()
    pipe_1[0].close()
    pipe_2[0].close()
    try:
        while True:
            print(pipe_2[1].recv())
    except EOFError:
        print("End")

结果为

0
1
4
9
16
25
36
49
64
81
End

Pipe() 函数返回一对通过双向管道连接起来的对象。在本例中, out_pipe 包含数字0-9,通过目标函数 create_items() 产生:

在第二个进程中,我们有两个管道,输入管道和包含结果的输出管道:

7.进程同步

多个进程可以协同工作来完成一项任务。通常需要共享数据。所以在多进程之间保持数据的一致性就很重要了。需要共享数据协同的进程必须以适当的策略来读写数据。相关的同步原语和线程的库很类似。

进程的同步原语如下:

  • Lock: 这个对象可以有两种状态:锁住(locked)和未锁住(unlocked)。一个Lock对象有两个方法, acquire() 和 release() ,来控制共享数据的读写权限。
  • Event: 实现了进程间的简单通讯,一个进程发事件的信号,另一个进程等待事件的信号。 Event 对象有两个方法, set() 和 clear() ,来管理自己内部的变量。
  • Condition: 此对象用来同步部分工作流程,在并行的进程中,有两个基本的方法: wait() 用来等待进程, notify_all() 用来通知所有等待此条件的进程。
  • Semaphore: 用来共享资源,例如,支持固定数量的共享连接。
  • Rlock: 递归锁对象。其用途和方法同 Threading 模块一样。
  • Barrier: 将程序分成几个阶段,适用于有些进程必须在某些特定进程之后执行。处于障碍(Barrier)之后的代码不能同处于障碍之前的代码并行。

下面的代码展示了如何使用 barrier() 函数来同步两个进程。我们有4个进程,进程1和进程2由barrier语句管理,进程3和进程4没有同步策略。

import multiprocessing
from multiprocessing import Barrier, Lock, Process
from time import time
from datetime import datetime

def test_with_barrier(synchronizer, serializer):
    name = multiprocessing.current_process().name
    synchronizer.wait()
    now = time()
    with serializer:
        print("process %s ----> %s" % (name, datetime.fromtimestamp(now)))

def test_without_barrier():
    name = multiprocessing.current_process().name
    now = time()
    print("process %s ----> %s" % (name, datetime.fromtimestamp(now)))

if __name__ == '__main__':
    synchronizer = Barrier(2)
    serializer = Lock()
    Process(name='p1 - test_with_barrier', target=test_with_barrier, args=(synchronizer,serializer)).start()
    Process(name='p2 - test_with_barrier', target=test_with_barrier, args=(synchronizer,serializer)).start()
    Process(name='p3 - test_without_barrier', target=test_without_barrier).start()
    Process(name='p4 - test_without_barrier', target=test_without_barrier).start()

在主程序中,我们创建了四个进程,然后我们需要一个锁和一个barrier来进程同步。barrier声明的第二个参数代表要管理的进程总数

test_with_barrier 函数调用了barrier的 wait() 方法

当两个进程都调用 wait() 方法的时候,它们会一起继续执行

barrier同步两个进程如下所示:

结果为

process p2 - test_with_barrier ----> 2023-09-18 16:22:20.998469
process p1 - test_with_barrier ----> 2023-09-18 16:22:20.998507
process p3 - test_without_barrier ----> 2023-09-18 16:22:20.999103
process p4 - test_without_barrier ----> 2023-09-18 16:22:20.999650

只能看到 with_barrier 的进程1和2比 without_barrier 的进程3和4时间差的小很多。偶尔进程1、2和进程3、4之间的时间是相同的

需要注意的是在macos时运行上述代码报错。

8.进程之间管理状态

Python的多进程模块提供了管理共享信息的管理者(Manager)。一个Manager对象控制着持有Python对象的服务进程,并允许其它进程操作共享对象。

Manager有以下特性:

  • 它控制着管理共享对象的服务进程
  • 它确保当某一进程修改了共享对象之后,所有的进程拿到额共享对象都得到了更新

代码如下

import multiprocessing

def worker(dictionary, key, item):
   dictionary[key] = item
   print("key = %d value = %d" % (key, item))

if __name__ == '__main__':
    mgr = multiprocessing.Manager()
    dictionary = mgr.dict()
    jobs = [multiprocessing.Process(target=worker, args=(dictionary, i, i*2)) for i in range(10)]
    for j in jobs:
        j.start()
    for j in jobs:
        j.join()
    print('Results:', dictionary)
  1. 首先,声明了一个manager

  2. 其次,创建了 dictionary 类型的一个数据结构,在 n 个 taskWorkers 之间共享,每个worker更新字典的某一个index。

  3. 所有的worker完成之后,新的列表打印到 stdout :

结果为

key = 0 value = 0
key = 1 value = 2
key = 2 value = 4
key = 3 value = 6
key = 4 value = 8
key = 5 value = 10
key = 6 value = 12
key = 8 value = 16
key = 7 value = 14
key = 9 value = 18
Results: {0: 0, 1: 2, 2: 4, 3: 6, 4: 8, 5: 10, 6: 12, 8: 16, 7: 14, 9: 18}

9.进程池

多进程库提供了 Pool 类来实现简单的多进程任务。 Pool 类有以下方法:

  • apply(): 直到得到结果之前一直阻塞。
  • apply_async(): 这是 apply() 方法的一个变体,返回的是一个result对象。这是一个异步的操作,在所有的子类执行之前不会锁住主进程。
  • map(): 这是内置的 map() 函数的并行版本。在得到结果之前一直阻塞,此方法将可迭代的数据的每一个元素作为进程池的一个任务来执行。
  • map_async(): 这是 map() 方法的一个变体,返回一个result对象。如果指定了回调函数,回调函数应该是callable的,并且只接受一个参数。当result准备好时会自动调用回调函数(除非调用失败)。回调函数应该立即完成,否则,持有result的进程将被阻塞。

下面的例子展示了如果通过进程池来执行一个并行应用。我们创建了有4个进程的进程池,然后使用 map() 方法进行一个简单的计算。

import multiprocessing

def function_square(data):
    result = data*data
    return result

if __name__ == '__main__':
    inputs = list(range(100))
    pool = multiprocessing.Pool(processes=4)
    pool_outputs = pool.map(function_square, inputs)
    pool.close()
    pool.join()
    print ('Pool    :', pool_outputs)
  1. multiprocessing.Pool 方法在输入元素上应用 function_square 方法来执行简单的计算。并行的进程数量是4
  2. pool.map 方法将一些独立的任务提交给进程池
  3. 计算的结果存储在 pool_outputs 中。最后的结果打印出来

结果为

Pool    : [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801]

需要注意的是, pool.map() 方法的结果和Python内置的 map() 结果是相同的,不同的是 pool.map() 是通过多个并行进程计算的。

在Python中,多个进程之间默认是无法直接共享变量的。每个进程都有自己独立的内存空间,变量在一个进程中的修改不会影响其他进程中的变量。

如果需要在多个进程之间共享变量,可以使用multiprocessing模块中的ValueArray来创建共享内存的变量。

进程池

import multiprocessing

def process_func(text, qq_status, index_data):
    # 处理文本段的逻辑
    # ...
    # 修改共享变量
    if qq_status.value == 0:
        try:
            # 处理文本段的逻辑
            # 如果处理失败,将qq_status赋值为-1
            # 如果处理成功,将文本段内容放入index_data中
            index_data.append(text)
            if (text == "中国最美丽的地方一定是新"):
                qq_status.value = -1
            print(text)
        except:
            qq_status.value = -1

def main():
    texts = ["中","中国","中国最","中国最美","中国最美丽","中国最美丽的","中国最美丽的地","中国最美丽的地方","中国最美丽的地方一","中国最美丽的地方一定",
            "中国最美丽的地方一定是","中国最美丽的地方一定是新","中国最美丽的地方一定是新疆","中国最美丽的地方是新疆省","中国最美丽的地方是新疆省阿"]  # 100个文本段
    manager = multiprocessing.Manager()
    qq_status = manager.Value('i', 0)
    index_data = manager.list()

    pool = multiprocessing.Pool()
    for text in texts:
        pool.apply_async(process_func, args=(text, qq_status, index_data))
    pool.close()
    pool.join()

    print(qq_status.value)
    print(index_data)

if __name__ == '__main__':
    main()

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值