首页 > 系统相关 >Python多进程中并行编程与进程池

Python多进程中并行编程与进程池

时间:2024-10-11 16:44:22浏览次数:7  
标签:function process Python 编程 Process time 进程 data

Python的并行编程可以采用multiprocessing或mpi4py模块来完成。

multiprocessing是Python标准库中的模块,实现了共享内存机制,也就是说,可以让运行在不同处理器核心的进程能读取共享内存。在基于共享内存通信的多进程编程中,常常通过加锁或类似机制来实现互斥。

mpi4py库实现了消息传递的编程范式(设计模式)。简单来说,就是进程之间不靠任何共享信息来进行通信(也叫做shared nothing),所有的交流都通过传递信息代替。在信息传递的代码中,进程通过send() 和receive()进行交流(与共享内存通信形成对比)。

1.Unix进程模型回顾

Python的multiprocessing进程模块在Unix上是基于fork()编程接口的(MacOS上默认是spawn()),故在介绍Python的并行编程模块之前,首先让我们回顾一下Unix的进程模型。

进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文(context)中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。进程提供给应用程序以下的抽象:

  • 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
  • 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。

进程间进行通信需要一些诸如共享内存的特殊手段,我们后文第3节部分会提到。

Unix系统上常通过以下方式创建子进程:

# include <stdlib.h>
# include <unistd.h>
# include <stdio.h>

int main(){
    pid_t pid;
    int x = 1;
    pid = fork();
    if (pid == 0){ //Child
        printf("child: x=%d\n", ++x);
        exit(0);
    }
    //Parent
    printf("parent: x=%d\n", --x);
    exit(0);
}

在Unix系统上运行这个程序时,我们得到下面的结果:

parent: x=0
child: x=2

fork()函数是一点需要注意的事它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork()返回子进程的PID;在子进程中,fork() 返回0。因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。

2.用multiprocessing模块创建进程

前面我们说过,在Unix下Python的multiprocessing模块实际上是调用fork()系统调用来创建进程的,这么坐会克隆出一个Python解释器,在fork()时会包含所有的状态,也即父进程的所有资源都由子进程继承。此外,父进程既可以在产生子进程之后继续异步执行,也可以暂停等待子进程创建完成之后再继续执行。Python的multiprocessing库通过以下几步创建进程:

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

如下所示我们创建了3个进程:

import multiprocessing
from multiprocessing import shared_memory
import os
import time

def foo(i, data):
    data[i] = i
    print ('called function in process: %d, data[%d] = %d' % (os.getpid(), i, data[i]))
    time.sleep(10)
    return

if __name__ == '__main__':

    n_processes = 8
    data = [None for i in range(n_processes)]
    
    begin = time.time()
    Process_jobs = []
    for i in range(n_processes):
        # 每个子进程都拿了一个data副本
        p = multiprocessing.Process(target=foo, args=(i, data))
        Process_jobs.append(p)
        p.start()
 
    for i in range(n_processes):
        Process_jobs[i].join()
    end = time.time()
    
    print(data)
    print("time: %d" % (end - begin))

代码打印输出:

called function in process: 39195, data[1] = 1
called function in process: 39198, data[4] = 4
called function in process: 39200, data[6] = 6
called function in process: 39197, data[3] = 3
called function in process: 39201, data[7] = 7
called function in process: 39196, data[2] = 2called function in process: 39194, data[0] = 0

called function in process: 39199, data[5] = 5
[None, None, None, None, None, None, None, None]
time: 11

可以看到子进程的打印是乱序的,说明我们不能对子进程的执行顺序做出任何假设。此外,可以看到每个子进程都拥有主进程中data的副本(也即地址空间独立),在子进程中对data进行修改并没有影响到我们主进程data中的值。最后,我们可以看到虽然我们每个子进程都会睡眠10s,但是整个程序的执行时间是11s,说明我们子进程的执行是并行的(本机为8核CPU)。

这里我们使用VSCode的调试插件,可以直观地看到子进程的创建情况:
在这里插入图片描述
诶,我们不是一共生成了8个子进程吗,为什么显示有9个子进程吗?这是因为我是在MacOS上测试的,而在Python3.8版本之后: 对于 MacOS,spawn()启动方式是默认方式。

因为 fork()可能导致subprocess崩溃。若以spawn()方式启动多进程会同时启动一个资源追踪进程,负责追踪当前程序的进程产生的、并且不再被使用的命名系统资源(如命名信号量以及SharedMemory对象)。当所有进程退出后,资源追踪会负责释放这些仍被追踪的的对象。

通常情况下是不会有这种对象的,但是假如一个子进程被某个信号杀死,就可能存在这一类资源的“泄露”情况。

3.共享内存

进程有独立的地址空间既是优点也是缺点。这样一来,一个进程不可能不小心覆盖另一个进程的虚拟内存,这就消除了许多令人迷惑的错误——这是一个明显的优点。另一方面,独立的地址空间使得进程共享状态信息变得更加困难。为了共享信息,它们必须使用显式的IPC(进程间通信)机制。基于进程的设计的另一个缺点是,它们往往比较慢,因为进程控制和IPC的开销很高。

进程间通信的手段有许多,包括管道、消息队列,系统V共享内存,系统V信号量(semaphore)、socket(不同主机上)等。Python的multiprocessing采用共享内存进行进程进程间通信,如下所示:

import multiprocessing
from multiprocessing import shared_memory
import os
import time

def foo(i, data):
    data[i] = i
    print ('called function in process: %d, data[%d] = %d' % (os.getpid(), i, data[i]))
    time.sleep(10)
    return

if __name__ == '__main__':

    n_processes = 8
    data = [None for i in range(n_processes)]
    # 需要置为共享内存
    data = shared_memory.ShareableList([999 for i in range(n_processes)])
    
    begin = time.time()
    Process_jobs = []
    for i in range(n_processes):
        # 每个子进程都拿了一个data副本
        p = multiprocessing.Process(target=foo, args=(i, data))
        Process_jobs.append(p)
        p.start()
 
    for i in range(n_processes):
        Process_jobs[i].join()
    end = time.time()

    print(data)
    print("time: %d" % (end - begin))
  
    data.shm.close()
    data.shm.unlink()

该代码运行结果如下:

called function in process: 43848, data[6] = 6
called function in process: 43842, data[0] = 0
called function in process: 43849, data[7] = 7
called function in process: 43843, data[1] = 1
called function in process: 43846, data[4] = 4
called function in process: 43844, data[2] = 2
called function in process: 43847, data[5] = 5
called function in process: 43845, data[3] = 3
ShareableList([0, 1, 2, 3, 4, 5, 6, 7], name='psm_c252aac3')
time: 11

可以看到8个子进程成功修改了共享内存中的内容。

4.终止/杀掉进程

我们可以使用terminate() 方法终止一个进程,这在Unix系统上是使用SIGTERM信号完成的。另外,我们可以使用 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()
    time.sleep(0.1) # 注意,传信号需要时间,故sleep一下
    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=50932 initial> False
Process running: <Process name='Process-1' pid=50944 parent=50932 started> True
Process terminated: <Process name='Process-1' pid=50944 parent=50932 stopped exitcode=-SIGTERM> False
Process joined: <Process name='Process-1' pid=50944 parent=50932 stopped exitcode=-SIGTERM> False
Process exit code: -15

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

  • == 0: 没有错误正常退出
  • > 0: 进程有错误,并以此状态码退出
  • < 0: 进程被-1 *信号杀死并以此作为 ExitCode 退出
  • 在我们的例子中,输出的ExitCode 是 -15 。负数表示子进程被数字为15的信号杀死。

如果我们修改为p.kill()则为立即杀掉一个进程。则上述程序会打印输出:

Process before execution: <Process name='Process-1' parent=52481 initial> False
Process running: <Process name='Process-1' pid=52492 parent=52481 started> True
Process terminated: <Process name='Process-1' pid=52492 parent=52481 stopped exitcode=-SIGKILL> False
Process joined: <Process name='Process-1' pid=52492 parent=52481 stopped exitcode=-SIGKILL> False
Process exit code: -9

也即子进程接收到数字为9的信号,即被杀死。终止进程和杀死进程的区别在于终止进程会先将数据由内存写入磁盘(类似于关机),但杀掉进程则不会保存数据(类似于直接给计算机断掉电源)。

Unix系统上常见信号查询手册如下:
在这里插入图片描述
最后,还需要强调一点,Python虽然提供了用信号杀掉进程的函数,但是并不提供用信号直接杀掉线程的函数,这是因为杀掉进程远比杀掉线程安全

5.进程间同步

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

进程的同步原语如下:

  • Lock: 这个对象可以有两种状态:锁住的(locked)和没锁住的(unlocked)。一个Lock对象有两个方法, acquire()和release() ,来控制共享数据的读写权限(如果使用with Lock():这样的语法,那么程序的控制流程在进入和离开with语句块时会自动调用acquire()和release()方法,我们将称with语句为创建了一个上下文环境(context)[8])。

  • 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) # Barrier声明的参数代表要管理的进程总数:
    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

以上代码运行结果如下:

process p3 - test_without_barrier ----> 2022-12-09 20:09:18.382580
process p1 - test_with_barrier ----> 2022-12-09 20:09:18.432348
process p2 - test_with_barrier ----> 2022-12-09 20:09:18.432586
process p4 - test_without_barrier ----> 2022-12-09 20:09:18.464562

可以看到with_barrier的进程1和进程2比without_barrier的进程3和4时间差的小很多。
关于用Barrier同步两个进程的过程,可描述如下图:
在这里插入图片描述

6.使用进程池

下面的例子展示了如果通过进程池来执行一个并行应用。我们创建了有8个进程的进程池,然后使用pool.map()方法来进行一个简单的并行计算(这里采样了函数式编程中的map-reduce思想),并与串行的map()方法进行对比。

import time
import multiprocessing

def function_square(x):
    return x * x

if __name__ ==  '__main__':
    data = [i for i in range(1000000)]

    # Parallel implementation
    begin = time.time()
    pool = multiprocessing.Pool(processes=8)
    results1 = pool.map(function_square, data)
    pool.close()
    pool.join()
    end = time.time()
    print("Parallel time: %f" % (end - begin))
    
    
    # Nonparallel code
    begin = time.time()
    results2 = map(function_square, data)
    end = time.time()
    print("Nonparallel time: %f" % (end-begin))

    if list(results1) == list(results2):
        print("Correct!")
        print(results1[:10])

上述代码运行结果如下:

Parallel time: 1.638756
Nonparallel time: 0.000003
Correct!
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

可见串行和并行代码运行结果相同,但是居然并行的时间更慢!这是由于我们计算的主要时间耗费在进程间通信上了(主进程需要将列表的各个部分拷贝到子进程),而每个子进程的计算量又很小,是典型的计算开销小于通信的情况,故不能取得理性的(8倍)的加速比。这告诉我们在使用Python多进程并行编程时要完成的工作规模必须足够大,这样可以弥补额外产生的通信开销。

这里需要说明一下使用Python进程池进行并行化处理时需要考虑的重要因素:

  • 这种并行化处理技术只适用于可以将问题分解成各个独立部分的情况。
  • 任务必须定义成普通的函数来提交。实例方法、闭包或者其他类型的可调用对象都是不支持并行处理的。
  • 函数的参数和返回值必须课兼容于pickle编码。任务的执行是在单独的解释器中完成的,这中间需要用到进程间通信。因此,在不同的解释器间交换数据必须要进行序列化处理(这一点尤其重要,在实际项目中我发现Pytorch的sparse_coo_tensor(类型就无法进行序列化),在调用p.start()时会报错NotImplementedError: Cannot access storage of SparseTensorImpl。

7.用多进程来规避GIL

多进程编程的一个好处就是可以用来规避GIL的限制。比如假设我们有以下线程代码:

# Performs a large calculation (CPU bound)
def some_work(args):
    ...
    return result

# A thread that calls the above function
def some_thread():
    while True:
        ...
        r = some_work(args)
        ...

我们可以采用如下方法将代码修改为使用进程池的方式:

import os
#学习中遇到问题没人解答?小编创建了一个Python学习交流群:531509025
# Processing pool (see below for initiazation)
pool = None

#  Performs a large calculation (CPU bound)
def some_work(args):
    ...
    return result

# A thread that calls the  above function
def some_thread():
    while True:
        ...
        r = pool.apply(some_work, (args))
        ...
        
# Initiaze the pool
if __name__ == "__main__":
    import multiprocessing
    pool = multiprocessing.Pool()

在上面这段代码中,每当有线程要执行CPU密集型的任务时,它就把任务提交到池中,然后进程池将任务转交给运行在另一个进程中的Python解释器。当线程等待结果的时候就会释放GIL。

此外,由于计算是在另一个单独的解释器中进行的,这样就不再受到GIL的限制了。在多核系统上,将会发现采用这种技术能轻易地利用到所有的CPU核心

标签:function,process,Python,编程,Process,time,进程,data
From: https://www.cnblogs.com/python1111/p/18458818

相关文章

  • Python闭包和保存自由变量
    1.闭包:用函数代替类术语闭包(closure)来自抽象代数。抽象代数里,一集元素称为在某个运算(操作)之下封闭,如果将该运算应用于这一集合中的元素,产出的仍然是该集合中的元素。然而在Python社区中还用术语“闭包”表述于此一个毫不相干的概念。注意我们文章中所称的闭包为Python中的闭包......
  • Python 枚举 Enum
    Python中的枚举数据类型(Enum)_pythonenum-CSDN博客默认属性name和valuefromenumimportEnumclassWeekday(Enum):monday=1tuesday=2wednesday=3thirsday=4friday=5saturday=6sunday=7print(Weekday.wednesday)......
  • verilog文件到tb的简单python脚本
    使用python3verilog_to_tb.pytest.v需要注意的是,例化时并没有去掉最后的逗号,手动去除下。verilog_to_tb.pyimportsysimportredefread_file(file):withopen(file,'r')asf:lines=f.readlines()returnlinesdefmain():lines=read_file('......
  • Python编程:创意爱心表白代码集
    在寻找一种特别的方式来表达你的爱意吗?使用Python编程,你可以创造出独一无二的爱心图案,为你的表白增添一份特别的浪漫。这里为你精选了六种不同风格的爱心表白代码,让你的创意和情感通过代码展现出来。话不多说,咱直接上代码!1.紫色浪漫:心形表白#1-1导入turtle模块进行设计......
  • Python 装饰器
    Python装饰器1.装饰器是什么装饰器是Python中的一种高级函数,用于在不修改原始函数代码的前提下,动态地为函数或类增加功能。它本质上是一个函数,接受另一个函数或类作为参数,并返回一个新的函数或类。装饰器的基本语法defdecorator(func):defwrapper(*args,**kwargs)......
  • 【Shell】基础的 shell 脚本编程入门
    目录注意点数值计算(())letexprbc基础条件测试test中括号双中括号各种比较逻辑注意点通配符和正则表达式不同符号含义不同,特别是*这个符号通配符:零到无限多个字符的意思正则表达式:重复零到无限多个前一个字符的意思${name}#取出变量结果$(date)#在括......
  • 教你如何免费获取股票数据用python、JavaScript (Node.js)、JAVA等多种语言的实例代码
    ​近一两年来,股票量化分析逐渐受到广泛关注。而作为这一领域的初学者,首先需要面对的挑战就是如何获取全面且准确的股票数据。因为无论是实时交易数据、历史交易记录、财务数据还是基本面信息,这些数据都是我们进行量化分析时不可或缺的宝贵资源。我们的核心任务是从这些数据......
  • 4 联合编程
    4联合编程C#连接相机添加相关的VisionPro控件打开vs在工具箱中右键=>选择项=>勾选CogAcqFifoEditV2(配置相机)、CogRecordDisplay(图片展示工具)​​将上面添加的两个控件都拖到窗体的合适位置隐藏代码中配置如下代码//取相工具CogAcqFifoToolacqFifoTool......
  • centos7 安装Python3及配置环境变量
    centos7安装Python3并配置环境变量亲测有效python官网(linux下载地址、版本可自选):https://www.python.org/ftp/python/一、添加阿里云第三方扩展源仓库(安装拓展源仓库才能安装yum安装openssl11openssl11-devel,python源码编译必须要openssl11或以上版本)备注,我这里是把pyhto......
  • 首个 AI 编程认证课程上线!阿里云 AI Clouder 认证:基于通义灵码实现高效 AI 编码
    最近两年,随着大语言模型和生成式AI技术的爆火,软件开发领域首当其冲成为了最热门的大模型应用场景之一,通义灵码等AI辅助编程工具纷纷问世。这些工具通过自然语言处理和机器学习技术,能够理解开发者的意图,并且提供行级/函数级代码、单元测试和代码注释的智能生成等功能,极大地提......