python基础知识学习——多进程编程

本文介绍了Python中的多进程编程,包括进程的定义、层次结构、并发现象,详细讲解了multiprocessing模块,如进程的开启、互斥锁、进程通信(Queue、Pipe、Managers)以及进程池的使用。通过实例展示了如何在Python中实现进程间的同步和数据共享,帮助理解Python多进程的优势和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.什么是进程

进程是系统中正在运行的一个程序,程序一旦运行就是进程。

进程可以看成程序执行的一个实例。进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。一个进程无法访问另一个进程的变量和数据结构,如果想让一个进程访问另一个进程的资源,需要使用进程间通信,比如管道,文件,套接字等。

一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。线程与进程的一个主要区别是,统一进程内的一个主要区别是,同一进程内的多个线程会共享部分状态,多个线程可以读写同一块内存(一个进程无法直接访问另一进程的内存)。同时,每个线程还拥有自己的寄存器和栈,其他线程可以读写这些栈内存。

2.进程的层次结构

无论UNIX还是windows,进程只有一个父进程,不同的是:

1. 在UNIX中所有的进程,都是以init进程为根,组成树形结构。父子进程共同组成一个进程组,这样,当从键盘发出一个信号时,该信号被送给当前与键盘相关的进程组中的所有成员。

2. 在windows中,没有进程层次的概念,所有的进程都是地位相同的,唯一类似于进程层次的暗示,是在创建进程时,父进程得到一个特别的令牌(称为句柄),该句柄可以用来控制子进程,但是父进程有权把该句柄传给其他子进程,这样就没有层次了。

3.进程并发的现象

进程并发的实现在于,硬件中断一个正在运行的进程,把此时进程运行的所有状态保存下来,为此,操作系统维护一张表格,即进程表(process table),每个进程占用一个进程表项(这些表项也称为进程控制块)

4.multiprocessing模块介绍

multiprocessing模块包含一个API,它基于threading API可以在多个进程中划分工作。由于Python全局解释器锁,python无法利用多个内核,我们可以通过multiprocessing来代替threading来利用多个CPU内核,进而解决计算瓶颈。
multiprocessing的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。

Process类的参数介绍描述
group参数未使用,值始终为None
target表示调用对象,即子进程要执行的任务
args表示调用对象的位置参数元组。如果没有参数,则为一个空元组
name子进程的名称
Process类的方法介绍
start()启动进程,并调用该子进程中的p.run()
run()进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法
terminate()强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。
is_alive()如果子进程仍然运行,返回True
join([timeout])主线程等待子进程终止(强调:是主线程处于等的状态,而子进程是处于运行的状态)。timeout是可选的超时时间,
需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程
Process类的属性介绍描述
daemon默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置
name进程的名称
pid进程的pid
ppid父进程的ip

5.multiprocessing模块的使用

开启进程的两种方式

第一种,创建子进程,最简单的方法就是用一个目标函数实例化一个Process对象,并调用start()方法让它开始工作。

#示例1
import multiprocessing
import time

def worker(name):
print("%s is working,id:%d" % (self.name, self.pid))
time.sleep(2)
print("%s is work ending,id:%d" % (self.name, self.pid))

if __name__ == '__main__':
    jobs = []
    p1 = multiprocessing.Process(target=worker, args= ("xiaoming",))
    p2 = multiprocessing.Process(target=worker, args= ("dong",))
    p1.start()
    p2.start()
    print('主进程')
    

第二种方法,我们可以通过Process派生出一个定制子类。

mport multiprocessing
import time

class MyProcess(multiprocessing.Process):
    def __init__(self, name):
        super().__init__()  # 必须继承父类的一些属性
        self.name = name
    def run(self):
        print("%s is working,id:%d" % (self.name,self.pid))
        time.sleep(2)
        print("%s is work ending,id:%d" % (self.name,self.pid))

if __name__ == '__main__':
    jobs = []
    p1 = MyProcess("xiaoming")
    p2 = MyProcess("dong")
    p1.start()
    p2.start()
    print('主进程')

互斥锁

进程之间数据隔离,但是共享一套文件系统,因而可以通过文件来实现进程直接的通信,但问题是必须自己加锁处理。

注意:加锁的目的是为了保证多个进程修改同一块数据时,同一时间只能有一个修改,即串行的修改,没错,速度是慢了,牺牲了速度而保证了数据安全。

import multiprocessing
import os
import time
def work(mutex):
    mutex.acquire()
    print('task[%s] is working'%os.getpid())
    time.sleep(3)
    print('task[%s] is work ending'%os.getpid())
    mutex.release()
if __name__ == '__main__':
    mutex = multiprocessing.Lock()
    p1 = multiprocessing.Process(target=work,args=(mutex,))
    p2 = multiprocessing.Process(target=work,args=(mutex,))

    p1.start()
    p2.start()
    p1.join()
    p2.join()
    print('主')

确定当前进程

传递参数来标识或命名进程很麻烦,而且没有必要。每个Process示实例都有一个名称,其默认值可以在创建进程时改变。给进程命名对于追踪进程是很有帮助的,特别是当应用中有多种类型的进程同时运行时。

import multiprocessing
import time

def work():
    name = multiprocessing.current_process().name
    print(name, "Starting")
    time.sleep(2)
    print(name, "Exiting")

def my_service():
    name = multiprocessing.current_process().name
    print(name, "Starting")
    time.sleep(3)
    print(name, "Exiting")

if __name__ == '__main__':
    service = multiprocessing.Process(name="my_service",
                                      target=my_service,
                                      args=())

    work_1 = multiprocessing.Process(name = "work_1",
                                     target=work,
                                     args=())
    work_2 = multiprocessing.Process(target=work,
                                     args=())
    work_1.start()
    work_2.start()
    service.start()
    

调试输出中,每行都包含当前进程的名称。进程名称列为Process-3的行对应为命名的进程work_1。对于未命名的进程,系统默认命名为 Process- 的形式。

守护进程

默认情况下,在所有子进程退出之前主程序不会退出。有些情况下,可能需要启动一个后台进程。它可以一直运行而步阻塞主进程退出,如果一个服务无法用一个容易的方法来中断进程,或者希望进程工作到一半时中止而不损失或破坏数据,对于这些服务,使用守护进程就很有用了。
要标识一个进程为守护进程,可以将其daemon属性设置为True。默认情况下进程不作为守护进程。

import multiprocessing
import time
import sys

def daemon():
    p = multiprocessing.current_process()
    print("Starting:", p.name, p.pid)
    sys.stdout.flush()
    time.sleep(2)
    print("Exiting:", p.name, p.pid)
    sys.stdout.flush()

def non_daemon():
    p = multiprocessing.current_process()
    print("Starting:", p.name, p.pid)
    sys.stdout.flush()
    print("Exiting:", p.name, p.pid)
    sys.stdout.flush()

if __name__ == '__main__':
    d = multiprocessing.Process(name="daemon",
                                target=daemon)
    d.daemon = True

    n = multiprocessing.Process(name = "non_daemon",
                                target=non_daemon)
    n.daemon = False

    d.start()
    time.sleep(2)
    n.start()

调试输出中,没有守护进程的Exiting消息,因为在守护进程从其2秒的睡眠时间唤醒之前,所有的非守护进程(包括主进程)已经退出。

终止进程

如果一个进程看起来已经挂起或者陷入死锁,则需要能够强制性地将其结束。对一个进程对象调用terminate()会结束子进程。

import multiprocessing
import time

def slow_worker():
    print("Starting worker")
    time.sleep(0.1)
    print("Finished worker")

if __name__ == '__main__':
    p = multiprocessing.Process(target=slow_worker)
    print("Before:", p , p.is_alive())

    p.start()
    print("During", p ,p.is_alive())

    p.terminate()
    print("Terminated", p, p.is_alive())

    p.join()
    print("Joined:", p, p.is_alive())

注意点:终止进程后要用join()退出进程,使进程管理代码有时间更新对象的状态,以反映进程已经终止。

6.进程通信

进程通信有3种模式:

  1. 通过multiprocessing.Queue(),该模式的数据通信会消耗大量资源。
  2. 管道multiprocessing.Pipe()
  3. 数据共享 Managers()

Queue队列传递

类似于线程,对于多个进程,一种常用的模式是将一个工作划分为多个工作进程中并行地运行。要想有效地使用多个进程,通常要求它们之间有某种通信,这样它们才能够分解工作,并完成结果的汇总。利用multiprocessing完成进程间的通信的一种简单的方法就是使用一个Queue来回传递消息。

import time
from multiprocessing import Process
import multiprocessing

def foo(q):
    time.sleep(2)
    q.put(12)
    q.put("baidu")
    q.put({"name": "baidu"})

if __name__ == '__main__':

    q = multiprocessing.Queue()

    p = Process(target=foo, args=(q,))
    p.start()

    print(q.get())
    print(q.get())
    print(q.get())

Pipe管道通信

管道相当于队列,但是管道不自动加锁

import multiprocessing

def f(conn):
    conn.send("Hello Dad")
    response = conn.recv()
    print(response)
    print("来自子进程的conn_id",id(conn))
    conn.close()

if __name__ == '__main__':
    parent_conn, son_conn = multiprocessing.Pipe()  #双向管道,返回到是一个元组的形式

    p = multiprocessing.Process(target=f, args=(son_conn,))
    p.start()
    print("parent_conn_id",id(parent_conn))
    print("来自父进程的son_conn_id",id(son_conn))
    print(parent_conn.recv())
    parent_conn.send("hello")
    p.join()

需要注意的是,这里的发送消息与socket模块中的send是不一样的,虽然两个模块的接口一样。因为socket模块中的send发送消息需要通过底层,那么只能通过字节的形式来发送。与进程中的数据共享有点区别。

Managers数据共享

Queue和Pipe只实现了的是数据交互,并没有实现数据共享,即一个进程更改另一个进程的数据。
通过Manager创建的特殊类型列表对象集中维护活动进程列表。Manager负责协调其所有用户之间的共享信息状态。
通过Managers()管理器来创建列表,这个列表将会共享,所有进程都能看到列表更新。除了列表,管理器还支持字典。

import multiprocessing

def worker(d, key, value):
    d[key] = value

if __name__ == '__main__':
    manager = multiprocessing.Manager()
    # d = {}
    d = manager.dict()

    jobs = [ multiprocessing.Process(target=worker, args=(d, i, i*2 )) for i in range(10)]
    for j in jobs:
        j.start()
    for j in jobs:
        j.join()
    print("Results:",d)

共享命名空间,除了字典和列表之外,Manager还可以创建一个共享Namespace。

import multiprocessing

def producer(ns, event):
    ns.value = "This is the value"
    event.set()

def consumer(ns, event):
    try:
        value = ns.value
    except Exception as err:
        print("Before event, error:",err)
    event.wait()
    print("After event:", ns.value)

if __name__ == '__main__':
    manager = multiprocessing.Manager()
    namespace = manager.Namespace()
    event = multiprocessing.Event()
    p = multiprocessing.Process(target=producer, args=(namespace, event))
    c = multiprocessing.Process(target=consumer, args=(namespace, event))
    c.start()
    p.start()
    c.join()
    p.join()

添加到Namespace的所有命名值对所有接收Namespace实例的客户可见。但是对命名空间种可变值内容的更新不会自动传播。

import multiprocessing

def producer(ns, event):

    ns.my_list.append("This is the value")

    event.set()

def consumer(ns, event):
    print("Before event:", ns.my_list)
    event.wait()
    print("After event:", ns.my_list)

if __name__ == '__main__':
    manager = multiprocessing.Manager()
    namespace = manager.Namespace()
    namespace.my_list = []

    event = multiprocessing.Event()
    p = multiprocessing.Process(target=producer,
                                args=(namespace, event))
    c = multiprocessing.Process(target=consumer,
                                args=(namespace, event))

    c.start()
    p.start()
    c.join()
    p.join()

如果要更新这个列表,那么就需要将它再次关联到命名空间对象。

7.进程池

有些情况下,所要完成的工作可以分解并独立地分布到多个工作进程,对于这种简单地情况,可以使用Pool类来管理固定数目的工作进程。多进程是实现并发的手段之一,需要注意的问题是:

  1. 很明显需要并发执行的任务通常要远大于核数
  2. 一个操作系统不可能无限开启进程,通常有几个核就开几个进程,进程开启过多,效率反而会下降(开启进程是需要占用系统资源的,而且开启多余核数目的进程也无法做到并行)

那么什么是进程池呢?进程池就是控制进程数目

ps:对于远程过程调用的高级应用程序而言,应该使用进程池,Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。

Pool类的参数描述
process要创建的进程数,如果省略,将默认为cpu_count()的值,可os.cpu_count()查看
initializer是每个工作进程启动时要执行的可调用对象,默认为None
initargs是要传给initializer的参数组
maxtasksperchild表示每个进程执行task的最大数目,设置maxtasksperchild参数可以告诉池在完成一定数量任务之后重新启动一个工作进程,来避免运行时间很长的工作进程消耗太多的系统资源。
ontext
Pool类的方法描述
apply_async (func, args=(), kwds={}, callback=None,error_callback=None)使用非阻塞方式调用指定方法,并行执行(同时执行)。如果callback指定,当结果可用时,结果会调用callback,callback属于主进程 。注意点:返回的值为一个对象,返回值用.get方法获取。
apply (func, args=(), kwds={})使用阻塞方式调用指定方法,阻塞方式就是要等上一个进程退出后,下一个进程才开始运行。
close()关闭进程池,不再接受进的进程请求,但已经接受的进程还是会继续执行。
terminate()不管程任务是否完成,立即结束。
join()主进程堵塞(就是不执行join下面的语句),直到子进程结束,注意,该方法必须在close或terminate之后使用。
map(func,iterable,chunksize)将可调用对象func应用给iterable的每一项,然后以列表形式返回结果,通过将iterable划分为多块,并分配给工作进程,可以并行执行。chunksize指定每块中的项数,如果数据量较大,可以增大chunksize的值来提升性能。
map_async(func,iterable,chunksize,callback)与map方法不同之处是返回结果是异步的,如果callback指定,当结果可用时,结果会调用callback,callback属于主进程 。
imap(func,iterable,chunksize)与map()方法的不同之处是返回迭代器而非列表。
imap_unordered(func,iterable,chunksize)与imap()不同之处是:结果的顺序是根据从工作进程接收到的时间而定的。
get(timeout)如果没有设置timeout,将会一直等待结果,如果设置了timeout,超过timeout将引发multiprocessing.TimeoutError异常。
ready()如果调用完成,返回True
successful()如果调用完成并且没有引发异常,返回True,如果在结果就绪之前调用,将引发AssertionError异常。
wait(timeout)等待结果变为可用,timeout为等待时间。

apply()

mport multiprocessing
import os,time
def task(n):
    print('task[%s] is working'%os.getpid())
    time.sleep(2)
    print('task[%s] finished'%os.getpid())
    return n**2
if __name__ == '__main__':
    p = multiprocessing.Pool(4) #最大四个进程
    res_list = []
    for i in range(10):#
        res = p.apply(task,args=(i,))  #同步的,等着一个运行完才执行另一个
        res_list.append(res)
        print('本次任务的结束:',res_list)
    p.close()#禁止往进程池内在添加任务
    p.join() #在等进程池
    print('主')

apply()方法是阻塞主进程, 并且一个一个按顺序地执行子进程, 等到全部子进程都执行完毕后 ,继续执行 apply()后面主进程的代码。

apply_async()

#错误版本
import multiprocessing
import os,time
def task(n):
    print('task[%s] running...'%os.getpid())
    time.sleep(3)
    return n**2
if __name__ == '__main__':
     start = time.time()
     p = multiprocessing.Pool(4)
     res_obj_l = []
     for i in range(10):
         res = p.apply_async(task,args=(i,)) #res为一个对象,若要取得task的返回值,则用.get方法
         res_obj_l.append(res.get())

     p.close() #禁止往进程池里添加任务
     # p.join()
     print(res_obj_l)
     print(time.time()-start)

其实我们通过现象发现该程序是阻塞的,即便没有p.join()方法。该程序也会等子进程运行结束后才能够运行主程序,那么,阻塞的意义何在?
原因是apply_async后面 get()等待线程运行结束才会下一个,而apply_async刚好又是异步以主程序为主的,所以这段代码实际变成了阻塞。

#正确版本
import multiprocessing
import os,time
def task(n):
    print('task[%s] running...'%os.getpid())
    time.sleep(3)
    return n**2
if __name__ == '__main__':
     start = time.time()
     p = multiprocessing.Pool(4)
     res_obj_l = []
     for i in range(10):
         res = p.apply_async(task,args=(i,)) #res为一个对象,若要取得task的返回值,则用.get方法
         res_obj_l.append(res)

     p.close() #禁止往进程池里添加任务
     p.join()
     print([obj.get() for obj in res_obj_l])  #这样就得到了就得到了
     print(time.time()-start)
     
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值