python进程、线程和协程02--进程

参考文档:https://docs.python.org/zh-cn/3.8/library/multiprocessing.html

参考文档:《Python核心编程(第3版 2016)》

1、进程简介

  • 程序是存储在磁盘上的可执行二进制(或其他类型)文件
  • 进程(有时称为重量级进程)则是一个执行中的程序。即把程序加载到内存中并被操作系统调用,拥有生命周期
  • 每个进程都拥有自己的地址空间内存数据栈以及其他用于跟踪执行的辅助数据
  • 进程也可以通过派生(fork 或spawn)新的进程来执行其他任务,不过因为每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的方式共享信息。
  • 操作系统管理所有进程的执行,并为这些进程合理地分配CPU时间。
  • 进程的优点:
    • 稳定性高,一个进程崩溃了,不会影响其他进程。
  • 进程的缺点:
    • 创建进程开销巨大。
    • 操作系统能同时运行进程数量有限。
  • multiprocessing模块操作进程。
    • multiprocessing模块同时提供了本地和远程并发操作。
    • multiprocessing模块通过使用子进程而非线程有效地绕过全局解释器锁。因此,允许程序员充分利用给定机器上的多个处理器。
    • multiprocessing模块在Unix和Windows上均被支持。

2、创建进程(multiprocessing.Process)

  • 在multiprocessing中,通过创建一个Process对象然后调用它的start()方法来生成进程

1、Process类

  • from multiprocessing import Process                                                                  #导入模块中的类
  • process=Process(target=函数,name=逆程的名字,args=(给函数传递的参数))    #创建进程对象
  • process.start()                                                                                                     #启动进程并执行任务
  • process.run()                                                                                                       #只是执行了任务但是没有启动进程
  • process.terminate()                                                                                             #终止进程

1、Process类的构造函数

class multiprocessing.Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

  • 进程对象表示在单独进程中运行的活动。
  • 应始终使用关键字参数调用构造函数。
    • group应该始终是None;它仅用于兼容threading.Thread。
    • target是由run()方法调用的可调用对象。它默认为None,意味着什么都没有被调用。
    • name是进程名称。
    • args是目标调用的参数元组。
    • kwargs是目标调用的关键字参数字典。
    • daemon的值是True、False或None。如果是None(默认值),则该标志将从创建的进程继承。(python3.3加入)
  • 如果子类重写构造函数,它必须确保它在对进程执行任何其他操作之前调用基类构造函数(Process.__init__())。

2、Process对象的方法

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

1、start()

  • 启动进程活动。
  • 这个方法每个进程对象最多只能调用一次。它会将对象的run()方法安排在一个单独的进程中调用。

2、run()

  • 表示进程活动的方法。
  • 可以在子类中重写此方法。标准run()方法会调用target参数传递给对象构造函数的可调用对象(如果有),分别从args和kwargs参数中获取顺序和关键字参数。

3、join([timeout])

  • 阻塞调用这个方法的进程。
  • 可选参数timeout方法
    • 如果可选参数timeout是None(默认值),则该方法将阻塞,直到调用join()方法的进程终止。
    • 如果timeout是一个正数,它最多会阻塞timeout秒。
    • 注意,如果进程终止或方法超时,则该方法返回None。检查进程的exitcode以确定它是否终止。
  • 一个进程可以被join多次。
  • 进程无法join自身,因为这会导致死锁。

4、is_alive()

  • 返回进程是否还活着。
  • 粗略地说,从start()方法返回到子进程终止之前,进程对象仍处于活动状态。

5、name

  • 进程的名称。该名称是一个字符串,仅用于识别目的。可以为多个进程指定相同的名称。
  • 初始名称由构造器设定。如果没有为构造器提供显式名称,则会构造一个形式为'Process-N1、Process-N2、...、Process-Nk'的名称,其中每个Nk是其父亲的第N个孩子。

6、daemon

  • 进程的守护标志,一个布尔值。这必须在start()被调用之前设置。
  • 初始值继承自创建进程。
  • 当进程退出时,它会尝试终止其所有守护进程子进程。
  • 注意,不允许在守护进程中创建子进程。这是因为当守护进程由于父进程退出而中断时,其子进程会变成孤儿进程。另外,这些不是Unix守护进程或服务,它们是正常进程,如果非守护进程已经退出,它们将被终止(并且不被合并)。

除了threading.Thread的API,Process对象还支持以下属性和方法:

7、pid

  • 返回进程ID。在生成该进程之前,这将是None。

8、exitcode

  • 子进程的退出代码。如果进程尚未终止,这将是None。负值-N表示子进程被信号N终止。

9、authkey

  • 进程的身份验证密钥(字节字符串)。
  • 当multiprocessing初始化时,主进程使用os.urandom()分配一个随机字符串。
  • 当创建Process对象时,它将继承其父进程的身份验证密钥,尽管可以通过将authkey设置为另一个字节字符串来更改。

10、sentinel

  • 系统对象的数字句柄,当进程结束时将变为"ready"。
  • 如果要使用multiprocessing.connection.wait()一次等待多个事件,可以使用此值。否则调用join()更简单。
  • 在Windows上,这是一个操作系统句柄,可以与WaitForSingleObject和WaitForMultipleObjects系列API调用一起使用。在Unix上,这是一个文件描述符,可以使用来自select模块的原语。
  • 3.3新版功能。

11、terminate()

  • 终止进程。在Unix上,这是使用SIGTERM信号完成的;在Windows上使用TerminateProcess()。注意,不会执行退出处理程序和finally子句等。
  • 注意,进程的后代进程将不会被终止——它们将简单地变成孤立的。
  • 警告如果在关联进程使用管道或队列时使用此方法,则管道或队列可能会损坏,并可能无法被其他进程使用。类似地,如果进程已获得锁或信号量等,则终止它可能导致其他进程死锁。

12、kill()

  • 与terminate()相同,但在Unix上使用SIGKILL信号。
  • 3.7新版功能。

13、close()

  • 关闭Process对象,释放与之关联的所有资源。如果底层进程仍在运行,则会引发ValueError。一旦close()成功返回,Process对象的大多数其他方法和属性将引发ValueError。
  • 3.7新版功能。

2、创建进程--创建Process对象时传递函数

1、创建进程

  • 在主进程中创建各个子进程,主进程结束不会结束各个子进程。
from multiprocessing import Process
from time import sleep
import os
def task1():
    while True:
        sleep(1)
        print('任务1,进程ID:{},父进程ID:{}'.format(os.getpid(), os.getppid()))
def task2():
    while True:
        sleep(1)
        print('任务2,进程ID:{},父进程ID:{}'.format(os.getpid(), os.getppid()))
if __name__ == '__main__':                       #主进程
    print('主进程ID:{},父进程ID:{}'.format(os.getpid(), os.getppid()))
    p1 = Process(target=task1, name='任务1')      #创建子进程p1,将函数task1启动成一个子进程,该子进程的名字是‘任务1’
    p1.start()                                   #启动子进程p1
    p2 = Process(target=task2, name='任务2')      #创建子进程p2
    p2.start()                                   #启动子进程p2
    print('主进程结束。')                          #主进程结束

<<<
主进程ID:10440,父进程ID:2696
主进程结束。
任务1,进程ID:2928,父进程ID:10440
任务2,进程ID:7636,父进程ID:10440
......

2、为进程调用的函数传递参数和结束进程

  • 参数必须是可迭代的。以元组的形式传递一个参数需注意逗号,即args=(参数1,)。
from multiprocessing import Process
from time import sleep
import os
def task1(s, name):
    while True:
        sleep(s)
        print('任务1,进程ID:{},父进程ID:{},参数name:{}'.format(os.getpid(), os.getppid(), name))
def task2(s, name):
    while True:
        sleep(s)
        print('任务2,进程ID:{},父进程ID:{},参数name:{}'.format(os.getpid(), os.getppid(), name))
if __name__ == '__main__':
    print('主进程ID:{},父进程ID:{}'.format(os.getpid(), os.getppid()))
    p1 = Process(target=task1, name='任务1', args=(1, 'AA'))     #参数args必须是可迭代的
    p1.start()
    p2 = Process(target=task2, args=(2, 'BB'), name='任务2')
    p2.start()
    for i  in range(10):
        sleep(1)
        if i == 9:
            p1.terminate()                                      #停止子进程p1
            p2.terminate()
    print('主进程结束。')

<<<
主进程ID:8332,父进程ID:2696
任务1,进程ID:11560,父进程ID:8332,参数name:AA
任务2,进程ID:10476,父进程ID:8332,参数name:BB
......

3、主进程的变量

  • 不论是可变类型的还是不可变类型的主进程变量,每个进程(主进程和每个子进程)都会单独拥有一份,互不影响。
from multiprocessing import Process
from time import sleep
import os
n = 0              #不可变类型的变量
lst = [1, 2, 3]    #可变类型的变量
def task1(s):
    global n
    while True:
        sleep(s)
        n += 1
        lst.append(n)
        print('任务1,n:{},lst:{}'.format(n, lst))
def task2(s):
    global n
    while True:
        sleep(s)
        n += 1
        lst.append(n)
        print('任务2,n:{},lst:{}'.format(n, lst))
if __name__ == '__main__':
    print('主进程ID:{},父进程ID:{}'.format(os.getpid(), os.getppid()))
    p1 = Process(target=task1, args=(1,))
    p1.start()
    p2 = Process(target=task2, args=(2,))
    p2.start()
    while True:
        sleep(3)
        n += 1
        lst.append(n)
        print('主进程,n:{},lst:{}'.format(n, lst))

<<<
主进程ID:5504,父进程ID:2696
任务1,n:1,lst:[1, 2, 3, 1]
任务1,n:2,lst:[1, 2, 3, 1, 2]
任务2,n:1,lst:[1, 2, 3, 1]
主进程,n:1,lst:[1, 2, 3, 1]
任务1,n:3,lst:[1, 2, 3, 1, 2, 3]
任务2,n:2,lst:[1, 2, 3, 1, 2]
任务1,n:4,lst:[1, 2, 3, 1, 2, 3, 4]
任务1,n:5,lst:[1, 2, 3, 1, 2, 3, 4, 5]
主进程,n:2,lst:[1, 2, 3, 1, 2]
......

3、创建进程--通过派生Process的子类

  • 必须继承Process类,必须重写Process类的run方法
from multiprocessing import Process
from time import sleep
class MyProcess(Process):       #继承Process类
    def __init__(self, name):
        super(MyProcess, self).__init__()
        self.name = name
    def run(self):              #重写run方法
        n = 1
        while True:
            sleep(1)
            print('n:{},进程名:{}'.format(n, self.name))
            n += 1
if __name__ == '__main__':
    p1 = MyProcess('猪不戒')
    p1.start()
    p2 = MyProcess('傻和尚')
    p2.start()

<<<
n:1,进程名:傻和尚
n:1,进程名:猪不戒
......

3、进程的启动方法

  • 根据不同的平台,multiprocessing支持三种启动进程的方法。
  • 警告:'spawn'和'forkserver'启动方法目前不能用于Unix上的“冻结”可执行文件(例如,有类似PyInstaller和cx_Freeze的包产生的二进制文件)。'fork'启动方法可以使用。

1、spawn

  • 父进程会启动一个的python解释器进程。
  • 子进程将只继承那些运行进程对象的run()方法所必需的资源。特别地,来自父进程的非必需文件描述符和句柄将不会被继承。
  • 与使用fork或forkserver相比,使用这种方法启动进程相当慢。
  • 可在Unix和Windows上使用。Windows上的默认值。

2、fork

  • 父进程使用os.fork()来产生Python解释器分叉。
  • 当子进程开始时,它实际上与父进程相同。父进程的所有资源都被子进程继承。
  • 请注意,安全地分叉一个多线程进程是有问题的。
  • 仅在Unix上可用。Unix上的默认值。

3、forkserver

  • 当程序启动并选择forkserver启动方法时,将启动一个服务器进程。
  • 每当需要一个新进程时,父进程就连接到服务器进程,并请求它派生一个新进程。
  • forkserver进程是单线程的,所以使用os.fork()是安全的。没有不必要的资源被继承。
  • 在支持通过Unix管道传递文件描述符的Unix平台上可用。
  • 在Unix上通过spawn和forkserver方式启动多进程会同时启动一个资源追踪进程,负责追踪当前程序的进程产生的、并且不再被使用的命名系统资源(如命名信号量以及SharedMemory对象)。当所有进程退出后,资源追踪进程会负责释放这些仍被追踪的的对象。通常情况下是不会有这种对象的,但是假如一个子进程被某个信号杀死,就可能存在这一类资源的“泄露”情况。(泄露的信号量以及共享内存不会被释放,直到下一次系统重启,对于这两类资源来说,这是一个比较大的问题,因为操作系统允许的命名信号量的数量是有限的,而共享内存也会占据主内存的一片空间)

示例1:

  • 要选择一个启动方法,你应该在主模块的if __name__ == '__main__'子句中调用set_start_method()
  • 在程序中set_start_method()不应该被多次调用。
import multiprocessing
def foo(q):
    q.put('hello')

if __name__ == '__main__':
    multiprocessing.set_start_method('spawn')
    q = multiprocessing.Queue()
    p = multiprocessing.Process(target=foo, args=(q,))    #不同进程使用不同的数据空间,因此需要将队列对象通过参数传递给其他进程,与线程不同
    p.start()
    print(q.get())
    p.join()

示例2

  • 也可以使用get_context()来获取上下文对象。上下文对象与multiprocessing模块具有相同的API,并允许在同一程序中使用多种启动方法
  • 想要使用特定启动方法的库应该使用get_context(),以避免干扰库用户的选择。
  • 注意,对象在不同上下文创建的进程间可能并不兼容。特别是,使用fork上下文创建的锁不能传递给使用spawn或forkserver启动方法启动的进程。
import multiprocessing
def foo(q):
    q.put('hello')
if __name__ == '__main__':
    ctx = multiprocessing.get_context('spawn')    #获取上下文对象
    q = ctx.Queue()                               #上下文对象与multiprocessing模块具有相同的API
    p = ctx.Process(target=foo, args=(q,))        #不同进程使用不同的数据空间,因此需要将队列对象通过参数传递给其他进程,与线程不同
    p.start()
    print(q.get())
    p.join()

4、进程之间的通信

  • 使用多进程时,一般使用消息机制实现进程间通信,尽可能避免使用同步原语,例如锁。
  • 消息机制包含:Pipe()(可以用于在两个进程间传递消息),以及队列(能够在多个生产者和消费者之间通信)。
  • Queue、SimpleQueue以及JoinableQueue都是多生产者/多消费者,并且实现了FIFO的队列类型,其表现与标准库中的queue.Queue类相似。不同之处在于Queue缺少标准库的queue.Queue从Python2.5开始引入的task_done()和join()方法。
  • 如果你使用了JoinableQueue,那么你必须对每个已经移出队列的任务调用JoinableQueue.task_done()。不然的话用于统计未完成任务的信号量最终会溢出并抛出异常。

1、队列(multiprocessing.Queue)

  • multiprocessing.Queue类是一个近似queue.Queue的克隆。

1、Queue类的构造函数

class multiprocessing.Queue([maxsize])

  • 返回一个使用一个管道和一些锁/信号量实现的共享队列实例。当一个进程将一个对象放进队列中时,一个写入线程会启动并将对象从缓冲区写入管道中。(写入线程:当进程第一次将一个项目放入队列时,就会启动一个支线线程
  • 一旦超时,将抛出标准库 queue 模块中常见的异常 queue.Empty 和 queue.Full。

2、Queue对象的方法

  • 除了 task_done() 和 join() 之外,Queue实现了标准库类queue.Queue中所有的方法。

1、qsize()

  • 返回队列的大致长度。由于多线程或者多进程的上下文,这个数字是不可靠的。
  • 注意,在Unix平台上,例如Mac OS X,这个方法可能会抛出NotImplementedError异常,因为该平台没有实现sem_getvalue()。

2、empty()

  • 如果队列是空的,返回True,反之返回False。由于多线程或多进程的环境,该状态是不可靠的。

3、full()

  • 如果队列是满的,返回True,反之返回False。由于多线程或多进程的环境,该状态是不可靠的。

4、put(obj[, block[, timeout]])

  • 将obj放入队列。
  • 有两个可选参数block和timeout。
    • 当block=True(默认)时,插入是阻塞式的,阻塞时间由timeout决定。
      • 如果timeout=None(默认),将一直阻塞,直至有空闲插槽可用;
      • 如果timeout是个正数,将最多阻塞timeout秒,如果在这段时间没有可用的空闲插槽,将引发queue.Full异常。
    • 当block=False时,插入是非阻塞式的,即当插入时队列已满将抛出queue.Full异常。
  • 在3.8版更改:如果队列已经关闭,会抛出ValueError而不是AssertionError。

5、get([block[, timeout]])

  • 从队列中取出并返回对象。
  • 有两个可选参数block和timeout。
    • 当block=True(默认)时,读取是阻塞式的,阻塞时间由timeout决定。
      • 如果timeout=None(默认),将一直阻塞,直至元素可得到;
      • 如果timeout是个正数,将最多阻塞timeout秒,如果在这段时间内元素不能得到,将引发queue.Empty异常。
    • 当block=False时,读取是非阻塞式的,即当读取时队列为空将抛出queue.Empty异常。
  • 在3.8版更改:如果队列已经关闭,会抛出ValueError而不是OSError。

6、put_nowait(obj)

  • 相当于put(obj, False)。

multiprocessing.Queue类有一些在queue.Queue类中没有出现的方法。这些方法在大多数情形下并不是必须的。

7、close()

  • 指示当前进程将不会再往队列中放入对象。一旦所有缓冲区中的数据被写入管道之后,后台的线程会退出。这个方法在队列被gc回收时会自动调用。

8、join_thread()

  • 等待后台线程。这个方法仅在调用了close()方法之后可用。这会阻塞当前进程,直到后台线程退出,确保所有缓冲区中的数据都被写入管道中。
  • 默认情况下,如果一个不是队列创建者的进程试图退出,它会尝试等待这个队列的后台线程。这个进程可以使用cancel_join_thread()让join_thread()方法什么都不做直接跳过。

9、cancel_join_thread()

  • 防止join_thread()方法阻塞当前进程。具体而言,这防止进程退出时自动等待后台线程退出。详见join_thread()。
  • 可能这个方法称为”allow_exit_without_flush()“会更好。这有可能会导致正在排队进入队列的数据丢失,大多数情况下你不需要用到这个方法,仅当你不关心底层管道中可能丢失的数据,只是希望进程能够马上退出时使用。

示例

from multiprocessing import Queue
q = Queue(5)                         #def __init__(self, maxsize=0, *, ctx)    #如果不设置长度,默认为无限长
q.put('A')                           #def put(self, obj, block=True, timeout=None)
q.put('B')
q.put('C') 
print('队列数据数量:', q.qsize())      #def qsize(self)
print('队列最大长度:', q._maxsize)     #self._maxsize = maxsize
print('队列是否为空:', q.empty())      #def empty(self)
print('队列是否已满:', q.full())       #def full(self)
print('取数据:', q.get(timeout=1))    #def get(self, block=True, timeout=None)
print('取数据:', q.get(timeout=1))
print('取数据:', q.get(timeout=1))

<<<
队列数据数量: 3
队列最大长度: 5
队列是否为空 False
队列是否已满 False
取数据: A
取数据: B
取数据: C

2、队列(multiprocessing.SimpleQueue)

1、SimpleQueue类的构造函数

class multiprocessing.SimpleQueue

  • 这是一个简化的Queue类的实现,很像带锁的Pipe。

2、SimpleQueue对象的方法

1、empty()

  • 如果队列为空返回True,否则返回False。

2、get()

  • 从队列中移出并返回一个对象。

3、put(item)

  • 将item放入队列。

3、队列(multiprocessing.JoinableQueue)

1、JoinableQueue类的构造函数

class multiprocessing.JoinableQueue([maxsize])

  • JoinableQueue类是Queue的子类,额外添加了task_done()和join()方法。

2、JoinableQueue对象的方法

1、task_done()

  • 指出之前进入队列的任务已经完成。由队列的消费者进程使用。对于每次调用get()获取的任务,执行完成后调用task_done()告诉队列该任务已经处理完成。
  • 如果join()方法正在阻塞之中,该方法会在所有对象都被处理完的时候返回(即对之前使用put()放进队列中的所有对象都已经返回了对应的task_done())。
  • 如果被调用的次数多于放入队列中的项目数量,将引发ValueError异常。

2、join()

  • 阻塞至队列中所有的元素都被接收和处理完毕。
  • 当条目添加到队列的时候,未完成任务的计数就会增加。每当消费者进程调用task_done()表示这个条目已经被回收,该条目所有工作已经完成,未完成计数就会减少。当未完成计数降到零的时候,join()阻塞被解除。

4、管道(multiprocessing.Pipe)

class multiprocessing.Pipe([duplex])

  • Pipe()函数返回一对表示管道端点的Connection对象(conn1, conn2),分别表示管道的两端。每个端点对象都有send()和recv()方法(相互之间的)。
  • 如果duplex为True(默认值),则管道是双向的。如果duplex为False,则管道是单向的:conn1只能用于接收消息,而conn2只能用于发送消息。
  • 注意,如果两个进程(或线程)同时尝试读取或写入管道的同一端,则管道中的数据可能会损坏。当然,在不同进程中同时使用管道的不同端的情况下不存在损坏的风险。

5、实现进程间通信

示例1:队列

  • 队列是线程和进程安全的。
from multiprocessing import Process, Queue
def f(q):
    q.put([42, None, 'hello'])
if __name__ == '__main__':
    q = Queue()
    p = Process(target=f, args=(q,))    #不同进程使用不同的数据空间,因此需要将队列对象通过参数传递给其他进程,与线程不同
    p.start()
    print(q.get())
    p.join()

示例2:管道

from multiprocessing import Process, Pipe
def f(conn):
    conn.send([42, None, 'hello'])
    conn.close()
if __name__ == '__main__':
    parent_conn, child_conn = Pipe()
    p = Process(target=f, args=(child_conn,))
    p.start()
    print(parent_conn.recv())
    p.join()

示例3

from multiprocessing import Process, Queue
from time import sleep
def downlasd(q):
    imgages = ['girl.jpg', 'boy.jpg', 'man.jpg']
    for image  in imgages:
        print('正在下载:', image)
        sleep(0.5)
        q.put(image)
def getfile(q):
    while True:
        try:
            file = q.get(timeout=5)
            print('{}保存成功!'.format(file,))
        except:
            print('全部保存完毕!')
            break
if __name__ == '__main__':
    q = Queue(5)
    p1 = Process(target=downlasd, args=(q,))    #不同进程使用不同的数据空间,因此需要将队列对象通过参数传递给其他进程,与线程不同
    p2 = Process(target=getfile, args=(q,))
    p1.start()
    p1.join()
    p2.start()
    p2.join()
    print('OVER!!!') 

5、同步原语

  • 通常来说同步原语在多进程环境中并不像它们在多线程环境中那么必要。(同步原语在多线程中尽量不使用
  • 注意可以使用管理器对象创建同步原语。

1、原始锁(multiprocessing.Lock)

class multiprocessing.Lock

  • 原始锁对象,类似于threading.Lock。
  • 一旦一个进程或者线程拿到了锁,后续的任何其他进程或线程的其他请求都会被阻塞直到锁被释放。任何进程或线程都可以释放锁。
  • 除非另有说明,否则multiprocessing.Lock用于进程或者线程的概念和行为都和threading.Lock一致。
  • 注意:Lock实际上是一个工厂函数。它返回由默认上下文初始化的multiprocessing.synchronize.Lock对象。
  • Lock支持上下文管理器协议,因此可以在with语句中使用。

1、acquire(block=True, timeout=None)

  • 可以阻塞或非阻塞地获得锁。
  • 如果block参数为True(默认值)。若可以获得锁返回True,并将锁锁定;若不能获取锁,将发生阻塞,直到锁被释放或超时返回False。需要注意的是第一个参数名与threading.Lock.acquire()的不同。
  • 如果block参数为False,方法的调用将不会阻塞。如果锁当前处于锁住状态,将返回False;否则将锁设置成锁住状态,并返回True。
  • 当timeout是一个正浮点数时,会在等待锁的过程中最多阻塞等待timeout秒,
    当timeout是负数时,效果和timeout为0时一样,
    当timeout是None(默认值)时,等待时间是无限长。
    当block参数为False时,timeout并没有实际用处,会直接忽略。否则,函数会在拿到锁后返回True或者超时没拿到锁后返回False。
    注意:对于timeout参数是负数和None的情况,其行为与threading.Lock.acquire()是不一样的。

2、release()

  • 释放锁,可以在任何进程、线程使用,并不限于锁的拥有者。
  • 当尝试释放一个没有被持有的锁时,会抛出ValueError异常,除此之外其行为与threading.Lock.release()一样。

示例

from multiprocessing import Process, Lock
def f(l, i):
    l.acquire()
    try:
        print('hello world', i)
    finally:
        l.release()
if __name__ == '__main__':
    lock = Lock()
    for num  in range(10):
        Process(target=f, args=(lock, num)).start()    #不同进程使用不同的数据空间,因此需要将锁对象通过参数传递给其他进程,与线程不同

2、递归锁(multiprocessing.RLock)

class multiprocessing.RLock

  • 递归锁对象:类似于threading.RLock。
  • 递归锁必须由持有线程、进程亲自释放
  • 如果某个进程或者线程拿到了递归锁,这个进程或者线程可以再次拿到这个锁而不需要等待。但是这个进程或者线程的拿锁操作和释放锁操作的次数必须相同。
  • 注意:RLock是一个工厂函数,调用后返回一个使用默认context初始化的multiprocessing.synchronize.RLock实例。
  • RLock支持context manager协议,因此可在with语句内使用。

1、acquire(block=True, timeout=None)

  • 可以阻塞或非阻塞地获得锁。
  • 当block参数设置为True时,会一直阻塞直到锁处于空闲状态(没有被任何进程、线程拥有),除非当前进程或线程已经拥有了这把锁。然后当前进程/线程会持有这把锁(在锁没有其他持有者的情况下),锁内的递归等级加一,并返回True。注意,这个函数第一个参数的行为和threading.RLock.acquire()的实现有几个不同点,包括参数名本身。
  • 当block参数是False,将不会阻塞。如果此时锁被其他进程或者线程持有,当前进程、线程获取锁操作失败,锁的递归等级也不会改变,函数返回False;如果当前锁已经处于释放状态,则当前进程、线程则会拿到锁,并且锁内的递归等级加一,函数返回True。
  • timeout参数的使用方法及行为与Lock.acquire()一样。但是要注意timeout的其中一些行为和threading.RLock.acquire()中实现的行为是不同的。

2、release()

  • 释放锁,使锁内的递归等级减一。
    • 如果释放后锁内的递归等级降低为0,则会重置锁的状态为释放状态(即没有被任何进程、线程持有),重置后如果有有其他进程和线程在等待这把锁,他们中的一个会获得这个锁而继续运行。
    • 如果释放后锁内的递归等级还没到达0,则这个锁仍将保持未释放状态且当前进程和线程仍然是持有者。
  • 只有当前进程或线程是锁的持有者时,才允许调用这个方法。如果当前进程或线程不是这个锁的拥有者,或者这个锁处于已释放的状态(即没有任何拥有者),调用这个方法会抛出AssertionError异常。注意这里抛出的异常类型和threading.RLock.release()中实现的行为不一样。

3、其他同步原语

1、栅栏对象(multiprocessing.Barrier)

class multiprocessing.Barrier(parties[, action[, timeout]])

  • 类似threading.Barrier的栅栏对象
  • 3.3新版功能.

2、信号量(multiprocessing.BoundedSemaphore)

class multiprocessing.BoundedSemaphore([value])

  • 非常类似threading.BoundedSemaphore的有界信号量对象
  • 一个小小的不同在于,它的acquire方法的第一个参数名是和Lock.acquire()一样的block。

3、信号量(multiprocessing.Semaphore)

class multiprocessing.Semaphore([value])

  • 一种信号量对象:类似于threading.Semaphore。
  • 一个小小的不同在于,它的acquire方法的第一个参数名是和Lock.acquire()一样的block。

4、条件对象(multiprocessing.Condition)

class multiprocessing.Condition([lock])

  • 条件变量:threading.Condition的别名。
  • 指定的lock参数应该是multiprocessing模块中的Lock或者RLock对象。
  • 在3.3版更改:新增了wait_for()方法。

5、事件对象(multiprocessing.Event)

class multiprocessing.Event

  • thread.event的克隆。

6、进程池(multiprocessing.pool.Pool)

  • 主进程结束,进程池中子进程也都会被结束
  • 当需要创建的子进程数量不多时,可以直接利用multiprocessing中的Process动态成生多个进程,但如果要创建成百甚至上千个进程时,手动的去创建进程的工作量巨大,此时就可以用到multiprocessing模块提供的Pool方法。
  • 初始化Pool时,可以指定一个最大进程数。当新的请求提交到Pool中时,如果进程池还没有满,那么就会创建一个新的进程用来执行该请求;但如果进程池中的进程数已经达到指定的最大值,那么该请求就会等待,直到进程池中有进程结束,才会用刚结束的进程去执行该请求。
  • pool =Pool(max):创建进程池对象,并指定该进程池最多开启的进程数。
    pool.apply_async(func, args=(), kwds={}, callback=None, error_callback=None):非阻塞式调用,并创建进程。
    pool.apply(func, args=(), kwds={}):阻塞式调用,并创建进程。
    pool.close():停止添加进程。
    pool.join():让主进程让步,即先执行进程池中的进程,全部结束后再执行主进程。

1、Pool类

1、Pool类的构造函数

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

  • 进程池对象,它控制可以提交作业的工作进程池。它支持带有超时和回调的异步结果,以及一个并行的map(映射)实现。
  • processes是要使用的工作进程数目。如果processes为None,则使用os.cpu_count()返回的值。
  • 如果initializer不是None,则每个工作进程在启动时都会调用initializer(*initargs)。
  • Maxtasksperchild是一个工作进程在退出并被一个新的工作进程替换之前可以完成的任务数量,以释放未使用的资源。默认的maxtasksperchild是None,这意味着工作进程的生存时间和池的生存时间一样长。
  • context(上下文)可用于指定用于启动工作进程的上下文。通常使用multiprocessing.Pool()函数或上下文对象的pool()方法创建池。在这两种情况下,上下文都是适当设置的。
  • 请注意,池对象的方法只能由创建池的进程调用。

2、Pool对象的方法

1、apply(func[, args[, kwds]])

  • 使用args参数以及kwds命名参数调用func,它会返回结果前阻塞。这种情况下,apply_async()更适合并行化工作。另外func只会在一个进程池中的一个工作进程中执行。

2、apply_async(func[, args[, kwds[, callback[, error_callback]]]])

  • apply()方法的一个变种,返回一个AsyncResult对象。
  • 如果指定了callback,它必须是一个接受单个参数的可调用对象。当执行成功时,callback会被用于处理执行后的返回结果,否则,调用error_callback。
  • 如果指定了error_callback,它必须是一个接受单个参数的可调用对象。当目标函数执行失败时,会将抛出的异常对象作为参数传递给error_callback执行。
  • 回调函数应该立即执行完成,否则会阻塞负责处理结果的线程。

3、map(func, iterable[, chunksize])

  • 内置map()函数的并行版本(但它只支持一个iterable参数,对于多个可迭代对象请参阅starmap())。它会保持阻塞直到获得结果。
  • 这个方法会将可迭代对象分割为许多块,然后提交给进程池。可以将chunksize设置为一个正整数从而(近似)指定每个块的大小可以。
  • 注意对于很长的迭代对象,可能消耗很多内存。可以考虑使用imap()或imap_unordered()并且显示指定chunksize以提升效率。

4、map_async(func, iterable[, chunksize[, callback[, error_callback]]])

  • map()方法的一个变种,返回一个AsyncResult对象。
  • 如果指定了callback,它必须是一个接受单个参数的可调用对象。当执行成功时,callback会被用于处理执行后的返回结果,否则,调用error_callback。
  • 如果指定了error_callback,它必须是一个接受单个参数的可调用对象。当目标函数执行失败时,会将抛出的异常对象作为参数传递给error_callback执行。
  • 回调函数应该立即执行完成,否则会阻塞负责处理结果的线程。

5、imap(func, iterable[, chunksize])

  • map()的延迟执行版本。
  • chunksize参数的作用和map()方法的一样。对于很长的迭代器,给chunksize设置一个很大的值会比默认值1极大地加快执行速度。
  • 同样,如果chunksize是1,那么imap()方法所返回的迭代器的next()方法拥有一个可选的timeout参数:如果无法在timeout秒内执行得到结果,则``next(timeout)``会抛出multiprocessing.TimeoutError异常。

6、imap_unordered(func,iterable[,chunksize])

  • 和imap()相同,只不过通过迭代器返回的结果是任意的。(当进程池中只有一个工作进程的时候,返回结果的顺序才能认为是"有序"的)
  • starmap(func, iterable[, chunksize])
  • 和map()类似,不过iterable中的每一项会被解包再作为函数参数。
  • 比如可迭代对象[(1,2), (3, 4)]会转化为等价于[func(1,2), func(3,4)]的调用。
  • 3.3新版功能.

7、starmap_async(func, iterable[, chunksize[, callback[, error_callback]]])

  • 相当于starmap()与map_async()的结合,迭代iterable的每一项,解包作为func的参数并执行,返回用于获取结果的对象。
  • 3.3新版功能.

8、close()

  • 阻止后续任务提交到进程池,当所有任务执行完成后,工作进程会退出。

9、terminate()

  • 不必等待未完成的任务,立即停止工作进程。当进程池对象呗垃圾回收时,会立即调用terminate()。

10、join()

  • 等待工作进程结束。调用join()前必须先调用close()或者terminate()。

2、实现进程池

1、非阻塞式进程池

  • 非阻塞式可以同时开启多个进程,即可以并发执行。
  • 回调函数:统一处理各个进程的结果。进程会将其返回值当作参数传递给回调函数。
from multiprocessing import Pool
import time
import os
from random import random
def task(task_name):
    print('开始做任务:', task_name)
    start = time.time()
    time.sleep(random() * 2)
    end = time.time()
    return '完成任务:{},用时:{},进程ID:{}'.format(task_name, end - start, os.getpid())
container = []
def callback_func(n):    #回调函数,进程将task函数的返回值当作参数传递给回调函数。
    container.append(n)
if __name__ == '__main__':
    pool = Pool(2)       #创建进程池对象,进程池最多开启2进程
    tasks = ['听音乐', '读书', '看电影', '打游戏']
    for taskl  in tasks:
        pool.apply_async(task, args=(taskl,), callback=callback_func)    #调用回调函数callback=callback_func
    pool.close()         #停止添加进程
    pool.join()          #让主进程让步
    for i  in container:
        print(i)
    print('完事了')

<<<
开始做任务: 听音乐
开始做任务: 读书
开始做任务: 看电影
开始做任务: 打游戏
完成任务:读书,用时:1.2852697372436523,进程ID:11760      #注意进程ID         
完成任务:听音乐,用时:1.945946216583252,进程ID:6996
完成任务:打游戏,用时:0.20986127853393555,进程ID:6996
完成任务:看电影,用时:1.5021371841430664,进程ID:11760
完事了

2、阻塞式进程池

  • 阻塞式添加一个任务,执行一个任务。如果一个任务不结束,另一个任务进不来,即不能并发执行。
from multiprocessing import Pool
import time
import os
from random import random
def task(task_name):
    print('开始做任务:', task_name)
    start = time.time()
    time.sleep(random() * 2)
    end = time.time()
    print('完成任务:{},用时:{},进程ID:{}'.format(task_name, end - start, os.getpid()))
if __name__ == '__main__':
    pool = Pool(3)
    tasks = ['听音乐', '读书', '看电影', '打游戏']
    for taskl  in tasks:
        pool.apply(task, args=(taskl,))
    pool.close()
    pool.join()
    print('完事了')

<<<
开始做任务: 听音乐
完成任务:听音乐,用时:1.644073724746704,进程ID:11376    #注意进程ID
开始做任务: 读书
完成任务:读书,用时:0.23582077026367188,进程ID:11164 
开始做任务: 看电影
完成任务:看电影,用时:1.051027774810791,进程ID:8228
开始做任务: 打游戏
完成任务:打游戏,用时:0.6055409908294678,进程ID:11376
完事了

 

posted @ 2021-07-21 15:36  麦恒  阅读(180)  评论(0编辑  收藏  举报