Python多进程(process)(一)进程和进程池

Python实用教程_spiritx的博客-优快云博客

在Python中因为有GIL的原因,线程能提供的并发效果并不理想,Python在多核上如果要释放并发的性能,更多的依靠多进程,我们会比线程更深入的去学习多进程。

进程(Process),顾名思义,就是进行中的程序。有一句话说得好:程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体。进程是资源分配的最小单元,也就是说每个进程都有其单独的内存空间。

Unix/Linux系统通过fork系统调用创建一个进程,但是在Windows中并没有fork调用。但是别担心,Python中内置的multiprocessing模块是跨平台的,我们可以通过对multiprocess模块中的Process类进行实例化创建一个进程对象,如:

import os
from multiprocessing import Process

def run_a_sub_proc(name):
    print(f'子进程:{name}({os.getpid()})开始...')

if __name__ == '__main__':
    print(f'主进程({os.getpid()})开始...')
    # 通过对Process类进行实例化创建一个子进程
    p = Process(target=run_a_sub_proc, args=('测试进程', ))
    p.start()
    p.join()

Process对象

定义:

class multiprocessing.Process(group=Nonetarget=Nonename=Noneargs=()kwargs={}*daemon=None)

进程对象表示在单独进程中运行的活动。

Process 类拥有和 threading.Thread 等价的大部分方法。

  • 应始终使用关键字参数调用构造函数。
  • group 应该始终是 None ,它仅用于兼容 threading.Thread 。
  • target 是由 run() 方法调用的可调用对象,可以是一个函数,也可以是一个可调用的函数对象,默认为 None ,None意味着什么都没有被调用。
  • name 是进程名称。
  • args 是目标调用的参数元组,元组中的每个元素对应一个函数的参数。
  • kwargs 是目标调用的关键字参数字典。
  • daemon 将进程 daemon 标志设置为 True 或 False 。如果是 None (默认值),则该标志将从创建的进程继承。

在默认情况下,不会将任何参数传递给 target。 args 参数默认值为 (),可被用来指定要传递给 target 的参数列表或元组。

属性和方法

属性和方法名 说明
name 进程的名称。该名称是一个字符串,仅用于识别目的。它没有语义。可以为多个进程指定相同的名称。
初始名称由构造器设定。 如果没有为构造器提供显式名称,则会构造一个形式为 'Process-N1:N2:...:Nk' 的名称,其中每个 Nk 是其父亲的第 N 个孩子。
run() Process对象最重要的方法,表示进程活动的方法,可以在子类中重载此方法。
start() 启动进程活动。这个方法每个进程对象最多只能调用一次。它会将对象的 run() 方法安排在一个单独的进程中调用。
join([timeout]) 如果可选参数 timeout 是 None (默认值),则该方法将阻塞,直到调用 join() 方法的进程终止。如果 timeout 是一个正数,它最多会阻塞 timeout 秒。请注意,如果进程终止或方法超时,则该方法返回 None 。检查进程的 exitcode 以确定它是否终止。
一个进程可以被 join 多次。
进程无法join自身,因为这会导致死锁。尝试在启动进程之前join进程是错误的。
is_alive() 返回进程是否还活着。粗略地说,从 start() 方法返回到子进程终止之前,进程对象仍处于活动状态。
daemon 进程的守护标志,一个布尔值。这必须在 start() 被调用之前设置。
初始值继承自创建进程。
当进程退出时,它会尝试终止其所有守护进程子进程。
请注意,不允许在守护进程中创建子进程。这是因为当守护进程由于父进程退出而中断时,其子进程会变成孤儿进程。 另外,这些 不是 Unix 守护进程或服务,它们是正常进程,如果非守护进程已经退出,它们将被终止(并且不被合并)。
pid 返回进程ID。在生成该进程之前,这将是 None 。
exitcode 子进程的退出代码。如果该进程尚未终止则为 None 。
如果子进程的 run() 方法正常返回,退出代码将是 0 。 如果它通过 sys.exit() 终止,并有一个整数参数 N ,退出代码将是 N 。
如果子进程由于在 run() 内的未捕获异常而终止,退出代码将是 1 。 如果它是由信号 N 终止的,退出代码将是负值 -N 。
authkey 进程的身份验证密钥(字节字符串)。
当 multiprocessing 初始化时,主进程使用 os.urandom() 分配一个随机字符串。
当创建 Process 对象时,它将继承其父进程的身份验证密钥,可以通过将 authkey 设置为另一个字节字符串来更改。
sentinel 系统对象的数字句柄
terminate() 终止进程,在UNIX系统中,使用SIGTERM进行终止,在windows系统,调用TerminateProcess进行终止,请注意,进程的后代进程将不会被终止 —— 它们将简单地变成孤立的。
kill() 在UNIX系统中,使用SIGTERM终止进程
close() 关闭 Process 对象,释放与之关联的所有资源。如果底层进程仍在运行,则会引发 ValueError 。一旦 close() 成功返回, Process 对象的大多数其他方法和属性将引发 ValueError 。

注意 start() 、 join() 、 is_alive() 、 terminate() 和 exitcode 方法只能由创建进程对象的进程调用。

子进程的创建

与线程一样,子进程也有两种创建方式,一种是直接使用process对象来拉起一个对象,另外一种是使用process的子类。

通过Process创建子进程

和使用 thread 类创建子线程的方式非常类似,使用 Process 类创建实例化对象,其本质是调用该类的构造方法创建新进程。Process 类的构造方法格式如下:

def __init__(self,group=None,target=None,name=None,args=(),kwargs={})

其中,各个参数的含义为:

  • group:该参数未进行实现,不需要传参;
  • target:为新建进程指定执行任务,也就是指定一个函数或者一个可执行对象;
  • name:为新建进程设置名称;
  • args:为 target 参数指定的参数传递非关键字参数;
  • kwargs:为 target 参数指定的参数传递关键字参数。

import os, time
from multiprocessing import Process

URLS=['https://blog.youkuaiyun.com/spiritx/article/details/132783171',
      'https://blog.youkuaiyun.com/spiritx/article/details/132782806',
      'https://blog.youkuaiyun.com/spiritx/article/details/132778558',
      'https://blog.youkuaiyun.com/spiritx/article/details/132773698']

def downloadUrl(url, nsecs):
    time.sleep(1)
    print(f'进程ID={os.getpid()}, {__name__=} download: {url}')


# p1 = Process(target=downloadUrl, args=(URLS[0], 3)) #RuntimeError
# p1.start()
# downloadUrl(URLS[1], 4)
# p1.join()

if __name__ == '__main__':
    p1 = Process(target=downloadUrl, args=(URLS[0], 3))
    p2 = Process(target=downloadUrl, args=(URLS[1], 4))
    p1.start()
    p2.start()
    downloadUrl(URLS[3], 4)
    p1.join()
    p2.join()

‘’'
进程ID=64482, __name__='__main__' download: https://blog.youkuaiyun.com/spiritx/article/details/132773698
进程ID=64484, __name__='__mp_main__' download: https://blog.youkuaiyun.com/spiritx/article/details/132783171
进程ID=64485, __name__='__mp_main__' download: https://blog.youkuaiyun.com/spiritx/article/details/132782806
‘''

注意:因为进程和子进程复用同一套代码,一定要写if __name__ == '__main__':这段代码,否则会出现RuntimeError的异常。

从输出来看,子进程和父进程的__name__并不是一个,父进程是__main__,而子进程是__mp_main__

通过Process子类创建子进程

和使用 thread 子类创建线程的方式类似,除了直接使用 Process 类创建进程,还可以通过创建 Process 的子类来创建进程。
需要注意的是,在创建 Process 的子类时,需在子类重写 run() 方法。实际上,该方法所起到的作用,就如同第一种创建方式中 target 参数执行的函数。
另外,通过 Process 子类创建进程,和使用 Process 类一样,先创建该类的实例对象,然后调用 start() 方法启动该进程。下面程序演示如何通过 Process 子类创建一个进程。

import os, time
import multiprocessing

URLS=['https://blog.youkuaiyun.com/spiritx/article/details/132783171',
      'https://blog.youkuaiyun.com/spiritx/article/details/132782806',
      'https://blog.youkuaiyun.com/spiritx/article/details/132778558',
      'https://blog.youkuaiyun.com/spiritx/article/details/132773698']


def downloadUrl(url='', secs=1):
    time.sleep(secs)
    print(f'进程ID={os.getpid()}, {__name__=} download: {url}')


class MyChildProcess(multiprocessing.Process):
    def __init__(self, url, secs):
        #super.__init__(self) #TypeError: descriptor '__init__' requires a 'super' object but received a 'MyChildProcess'
        multiprocessing.Process.__init__(self)
        self.url = url
        self.secs = secs

    def run(self):
        downloadUrl(self.url, self.secs)



if __name__ == '__main__':
    p1 = MyChildProcess(URLS[0], 3)
    p2 = MyChildProcess(URLS[1], 4)
    p1.start()
    p2.start()
    downloadUrl(URLS[3], 4)
    p1.join()
    p2.join()

‘’'
进程ID=65688, __name__='__mp_main__' download: https://blog.youkuaiyun.com/spiritx/article/details/132783171
进程ID=65686, __name__='__main__' download: https://blog.youkuaiyun.com/spiritx/article/details/132773698
进程ID=65689, __name__='__mp_main__' download: https://blog.youkuaiyun.com/spiritx/article/details/132782806
‘''

有几个地方要特别注意:

  • 注意一定要重写__init__()和run()方法,如果不重写__init__()方法,参数要和Process一致。
  • 在子类的__init__()方法中一定要调用Process.__init__()方法,写super.__init__()方法会报TypeError的异常。

进程池

当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果是上百甚至上千个目标,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。

class multiprocessing.Pool([processes[, initializer[, initargs[, maxtasksperchild[, context]]]]])

  • processes — 进程池中进程数量,如果为 None,则使用 os.cpu_count() 返回的值
  • initializer — 如果该参数不为 None,则所有进程池中的进程启动时都会先执行 initializer(*initargs)
  • maxtasksperchild — 如果该参数不为 None,则进程在执行 maxtasksperchild 次任务后会被自动销毁、重启
  • context — 用于指定进程池中进程运行的上下文

初始化Pool时,可以指定一个最大进程数,当有新的请求提交到Pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到指定的最大值,那么该请求就会等待,直到池中有进程他的任务结束,新的任务会交给这个空闲进程去处理。

import os, time
import multiprocessing
import random

URLS=['https://blog.youkuaiyun.com/spiritx/article/details/132783171',
      'https://blog.youkuaiyun.com/spiritx/article/details/132782806',
      'https://blog.youkuaiyun.com/spiritx/article/details/132778558',
      'https://blog.youkuaiyun.com/spiritx/article/details/132773698',
      'https://blog.youkuaiyun.com/spiritx/article/details/5',
      'https://blog.youkuaiyun.com/spiritx/article/details/6',
      'https://blog.youkuaiyun.com/spiritx/article/details/7',
      'https://blog.youkuaiyun.com/spiritx/article/details/8',
      'https://blog.youkuaiyun.com/spiritx/article/details/9']

INDEXS=[1,2,3,4,5,6,7,8,9]
PARAMS=list(zip(URLS, INDEXS)) #通过zip()方法将多个参数压缩成list对象

def downloadUrl(url, index):
    #url, index = param  # 对多个参数进行解包
    print(f'进程ID={os.getpid()}, {__name__=} start download: {url=},{index=}')
    time.sleep(random.randint(1,3))
    print(f'进程ID={os.getpid()}, {__name__=} end download: {url=},{index=}')
    return index


if __name__ == '__main__':
    pool = multiprocessing.Pool(4)
    results = [pool.apply_async(downloadUrl, param) for param in PARAMS]
    pool.close()
    pool.join()

    for result in results:
        print(f'{result.get()=}')

    

‘’'
进程ID=75676, __name__='__mp_main__' start download: url='https://blog.youkuaiyun.com/spiritx/article/details/132783171',index=1
进程ID=75677, __name__='__mp_main__' start download: url='https://blog.youkuaiyun.com/spiritx/article/details/132782806',index=2
进程ID=75675, __name__&#
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值