python:并发编程(五)

前言

本文将和大家一起探讨python的多进程并发编程(中篇),使用内置基本库multiprocessing来实现并发,先通过官方来简单使用这个模块。先打好基础,能够有个基本的用法与认知,后续文章,我们再进行详细使用。

本文为python并发编程的第五篇,上一篇文章地址如下:

python:并发编程(四)_Lion King的博客-CSDN博客

下一篇文章地址如下:

python:并发编程(六)_Lion King的博客-CSDN博客

一、进程基础编程

1、进程间同步

本节将讨论锁的使用,值得注意的是,使用锁将导致性能下降,本章末尾再细聊。

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()  # 创建子进程并启动

上述代码使用了 multiprocessing 模块中的锁(Lock)实现了进程间的互斥访问。首先,我们创建了一个锁对象 lock。然后,通过循环创建了 10 个子进程,每个子进程都调用函数 f,并将锁对象 lock 和当前的 num 作为参数传递给函数。

在函数 f 中,子进程首先通过调用 l.acquire() 方法获取锁,表示开始进入临界区。在临界区内部,执行了打印输出语句,输出了 "hello world" 和当前的 num 值。最后,通过调用 l.release() 方法释放锁,表示离开临界区。

通过使用锁,我们确保了在任意时刻只有一个子进程可以进入临界区执行打印输出的代码块,避免了多个子进程同时访问临界区引起的竞争条件问题。

由于每个子进程都拥有独立的锁对象,它们可以并发地执行临界区的代码,互不干扰。这样就实现了进程间的互斥访问和数据共享。

2、进程间共享状态

官方表示共享状态应该尽量避免,这个我们本章末尾再聊。

共享内存

from multiprocessing import Process, Value, Array

def f(n, a):
    n.value = 3.1415927  # 修改共享变量的值为3.1415927
    for i in range(len(a)):
        a[i] = -a[i]  # 将整型数组中的元素取反

if __name__ == '__main__':
    num = Value('d', 0.0)  # 创建一个双精度浮点型的共享变量,初始值为0.0
    arr = Array('i', range(10))  # 创建一个整型数组,初始值为0到9

    p = Process(target=f, args=(num, arr))  # 创建子进程,并传入共享变量和整型数组作为参数
    p.start()  # 启动子进程
    p.join()  # 等待子进程执行完成

    print(num.value)  # 打印共享变量的值
    print(arr[:])  # 打印整型数组的元素

上述代码使用了multiprocessing模块中的ValueArray来在进程之间共享数据。具体说明如下:

(1)Value('d', 0.0)创建了一个双精度浮点型的共享变量num,初始值为0.0。

(2)Array('i', range(10))创建了一个整型数组arr,初始值为0到9。

(3)f函数是子进程要执行的任务函数。在该函数中,将共享变量num的值设置为3.1415927,并将整型数组arr中的元素取反。

(4)主进程创建了子进程p,并传入共享变量num和整型数组arr作为参数。

(5)子进程开始执行任务函数f,修改共享变量num的值为3.1415927,并将整型数组arr的元素取反。

(6)主进程使用p.join()等待子进程执行完成。

(7)主进程打印出共享变量num的值和整型数组arr的元素,观察修改后的结果。

通过使用ValueArray可以在多个进程之间共享数据,这样不同进程之间可以并发地对共享数据进行读写操作,实现数据共享和通信。在上述例子中,共享变量num和整型数组arr在主进程和子进程之间共享,子进程对它们进行了修改,主进程可以读取到修改后的结果。

服务进程

from multiprocessing import Process, Manager

def f(d, l):
    d[1] = '1'  # 在字典中插入键值对
    d['2'] = 2
    d[0.25] = None
    l.reverse()  # 反转列表元素的顺序

if __name__ == '__main__':
    with Manager() as manager:
        d = manager.dict()  # 创建一个可以在多个进程之间共享的字典
        l = manager.list(range(10))  # 创建一个可以在多个进程之间共享的列表

        p = Process(target=f, args=(d, l))  # 创建子进程,并传入共享字典和列表作为参数
        p.start()  # 启动子进程
        p.join()  # 等待子进程执行完成

        print(d)  # 打印共享字典的内容
        print(l)  # 打印共享列表的内容

上述代码演示了如何在多个进程之间共享数据使用Manager对象。具体步骤如下:

(1)导入必要的模块:Process用于创建进程,Manager用于创建进程间共享的Manager对象。

(2)定义一个函数f,该函数接收共享字典d和共享列表l作为参数。在函数内部,通过修改字典和列表的内容来演示数据的共享和修改。

(3)在if __name__ == '__main__':条件下,使用Manager创建一个Manager对象,作为数据共享的管理器。

(4)使用Manager对象的dict()方法创建一个共享字典d,并使用list()方法创建一个共享列表l

(5)创建子进程p,传入函数f和共享字典d、共享列表l作为参数。

(6)启动子进程p,子进程开始执行函数f

(7)主进程调用join()方法,等待子进程执行完成。

(8)打印共享字典d和共享列表l的内容,验证多个进程之间的数据共享和修改。

在代码执行过程中,共享字典d被子进程修改,插入了新的键值对;共享列表l被子进程修改,元素的顺序被反转。通过使用Manager对象创建的共享数据结构,实现了多个进程之间的数据共享和协同工作。

3、使用工作进程池

from multiprocessing import Pool, TimeoutError
import time
import os

def f(x):
    return x*x

if __name__ == '__main__':
    # 创建一个具有4个工作进程的进程池
    with Pool(processes=4) as pool:

        # 打印 "[0, 1, 4,..., 81]"
        print(pool.map(f, range(10)))

        # 以任意顺序打印相同的数字
        for i in pool.imap_unordered(f, range(10)):
            print(i)

        # 异步计算 "f(20)"
        res = pool.apply_async(f, (20,))      # 在 *一个* 进程中执行
        print(res.get(timeout=1))             # 打印 "400"

        # 异步计算 "os.getpid()"
        res = pool.apply_async(os.getpid, ()) # 在 *一个* 进程中执行
        print(res.get(timeout=1))             # 打印该进程的ID

        # 异步地启动多个计算 *可能* 使用更多进程
        multiple_results = [pool.apply_async(os.getpid, ()) for i in range(4)]
        print([res.get(timeout=1) for res in multiple_results])

        # 使单个工作进程休眠10秒
        res = pool.apply_async(time.sleep, (10,))
        try:
            print(res.get(timeout=1))
        except TimeoutError:
            print("我们缺乏耐心并得到了一个multiprocessing.TimeoutError")

        print("此时,进程池仍可用于更多工作")

    # 退出 'with' 块后,进程池被关闭
    print("现在进程池已关闭且不再可用")

上述代码展示了使用multiprocessing.Pool进行进程池编程的示例。具体解释如下:

(1)导入所需的模块:Pool用于创建进程池,TimeoutError用于处理超时异常,time用于时间相关操作,os用于获取进程ID。

(2)在if __name__ == '__main__':条件下,使用Pool创建一个进程池,指定processes参数为4,即创建4个工作进程。

(3)使用pool.map()方法将函数f应用于一个可迭代对象range(10),并返回结果列表。结果列表按照顺序打印了[0, 1, 4,..., 81]

(4)使用pool.imap_unordered()方法将函数f应用于一个可迭代对象range(10),并返回一个迭代器。通过迭代器按照任意顺序打印相同的数字。

(5)使用pool.apply_async()方法异步地对函数f应用于参数(20,),并返回一个AsyncResult对象。通过res.get(timeout=1)获取异步操作的结果,并打印结果400

(6)使用pool.apply_async()方法异步地对函数os.getpid应用于无参数,获取当前进程的ID,并打印该进程的ID。

(7)使用列表推导式和pool.apply_async()方法,异步地获取4个进程的ID,并打印结果。

(8)使用pool.apply_async()方法异步地对函数time.sleep()应用于参数(10,),使一个工作进程休眠10秒。由于设置了超时时间为1秒,捕获TimeoutError异常并打印相应信息。

(9)打印一条提示消息,表明进程池仍然可用于执行更多任务。

(10)当退出with块时,进程池被关闭,不再可用。

(11)打印一条消息,表明进程池已关闭并不再可用。

上述代码演示了使用multiprocessing.Pool进行进程池编程的常见用法,包括使用map()imap_unordered()方法并行处理任务,使用apply_async()方法异步执行任务,并获取异步操作的结果。

二、本章相关概念解析

1、进程间同步

进程间同步是指多个进程之间协调和同步它们的执行,以确保它们按照一定的顺序和时间进行操作。在并发编程中,进程间同步非常重要,因为多个进程并发执行时可能会出现竞态条件、资源争用、数据不一致等问题。通过进程间同步,可以保证进程按照预期的顺序执行,并避免出现潜在的问题。

常见的进程间同步方法包括:

(1)互斥锁(Mutex):通过互斥锁可以实现对共享资源的互斥访问,一次只允许一个进程或线程对资源进行操作。

(2)信号量(Semaphore):信号量用于控制对共享资源的访问数量,可以设置访问资源的进程或线程数目。

(3)事件(Event):事件用于进程间的通知和等待,一个进程可以等待其他进程触发某个事件的发生。

(4)条件变量(Condition):条件变量用于在某个条件满足时进行进程间的通信和同步。

(5)栅栏(Barrier):栅栏用于在多个进程或线程达到某个点之前进行等待,然后再同时继续执行。

这些进程间同步的机制可以确保进程按照一定的顺序、规则和时序进行操作,避免竞态条件和数据不一致等问题,保证系统的正确性和可靠性。

2、进程间共享状态

进程间共享状态是指多个进程之间共享数据或资源的一种机制。在并发编程中,不同的进程可能需要访问和操作相同的数据或资源,这就涉及到进程间共享状态的问题。

共享状态通常包括共享内存和共享文件两种形式:

(1)共享内存:多个进程可以映射到同一块物理内存区域,从而实现对内存数据的共享访问。进程可以通过读写共享内存来进行进程间通信和共享数据。共享内存的优势是速度快、效率高,但需要注意进程间对共享内存的访问同步和互斥,以避免竞态条件和数据不一致的问题。

(2)共享文件:多个进程可以通过操作共享的文件来进行进程间通信和数据共享。进程可以通过读写共享文件来交换数据和信息。共享文件的优势是适用于不同机器或进程之间的通信,但相比于共享内存,速度较慢。

3、解决进程间共享状态的方法

在 multiprocessing 中,确实有两种主要的方法可以实现多个进程之间的共享数据:服务进程和共享内存。

服务进程:

(1)概念:服务进程是一种基于进程间通信的机制,其中一个进程(服务进程)负责管理共享数据,并提供对该数据的访问和操作接口,其他进程通过与服务进程进行通信来使用共享数据。
(2)实现:服务进程通常使用 Queue、Pipe 或 Manager 等对象来实现进程间通信,这些对象提供了进程安全的数据结构,如队列、管道和共享字典等,用于传递和共享数据。
(3)特点:
①通过进程间通信实现数据共享。
②数据共享是通过对象的传递和交互来实现的。
③可以通过队列、管道等实现进程间的数据传输和同步。


共享内存:

(1)概念:见“进程间共享状态”的(1)
(2)实现:在 multiprocessing 中,可以使用 Value 和 Array 对象来创建共享内存,Value 用于共享单个值,Array 用于共享数组。
(3)特点:
①通过共享同一块物理内存实现数据共享。
②多个进程可以直接读写共享内存中的数据。
③需要注意对共享内存的访问同步,以防止竞态条件等问题。

区别:

(1)服务进程通过进程间通信实现数据共享,数据传输是通过消息传递的方式进行的,更适合于灵活的数据交换和同步。

(2)共享内存允许多个进程直接访问同一块内存区域,数据共享更高效,但需要注意同步和竞态条件的处理。

选择使用哪种方法取决于具体的应用需求和情况。如果需要更灵活的数据交换和同步,并且数据量较小,可以考虑使用服务进程。如果需要高效的数据共享,并且数据量较大,可以考虑使用共享内存。

4、有趣的共享内存

共享内存既是进程间共享状态之一,也是解决进程间共享状态的方法之一。

5、为什么并发编程应该避免共享状态

并发编程中共享状态是指多个线程或进程之间共享的数据或资源。避免共享状态的原因有以下几点:

(1)竞态条件:当多个线程或进程同时访问和修改共享状态时,可能会导致竞态条件的发生。竞态条件是指多个线程或进程在访问共享状态时的执行顺序不确定,导致结果的不确定性和不一致性。

(2)锁竞争:当多个线程或进程试图同时获取对共享状态的独占访问权时,会发生锁竞争。锁竞争会导致性能下降,因为线程或进程需要等待锁的释放才能继续执行。

(3)死锁:当多个线程或进程相互等待对方释放锁时,可能发生死锁。死锁是一种无法解决的状态,导致程序无法继续执行。

(4)难以调试:共享状态的多线程或多进程访问使得程序的行为更加复杂和难以预测,增加了调试的困难。

为了避免这些问题,可以采取以下策略:

(1)不可变数据:使用不可变数据结构可以避免对共享状态的修改,从而避免竞态条件和锁竞争。

(2)线程安全的数据结构:使用线程安全的数据结构,如线程安全的队列、字典或集合,可以避免显式的锁操作。

(3)分离状态:尽量避免多个线程或进程之间共享状态,通过将状态分离为独立的部分,每个线程或进程操作自己的状态,减少共享。

(4)同步机制:如果必须共享状态,使用适当的同步机制,如锁、条件变量、信号量等,来保证共享状态的一致性和避免竞态条件。

(5)并发容器:使用并发容器代替传统的数据结构,如并发队列、并发字典等,可以提供更好的线程安全性和并发性能。

总之,避免共享状态可以减少并发编程中的问题和复杂性,提高程序的可靠性和性能。

6、锁

在并发编程中,锁是一种同步机制,用于控制对共享资源的访问。锁可以防止多个线程或进程同时访问共享资源,从而避免竞态条件和数据不一致的问题。

锁有两种状态:锁定和非锁定。一次只能有一个线程或进程获得锁,获得锁的线程或进程可以访问共享资源,而其他线程或进程则需要等待锁的释放才能继续访问。这样可以保证同一时间只有一个线程或进程在访问共享资源,从而避免了竞态条件。

使用锁的并发编程可以解决多个线程或进程对共享状态的并发访问问题,但也会带来一些缺点,包括:

(1)竞争和性能问题:使用锁会引入竞争条件,当多个线程或进程试图获取同一个锁时,只有一个线程或进程能够获得锁,其他线程或进程需要等待。这可能导致性能下降,尤其在高并发情况下。

(2)死锁风险:如果在并发程序中不正确地使用锁,可能会导致死锁。死锁是指多个线程或进程相互等待对方持有的锁而无法继续执行的情况。

(3)复杂性增加:使用锁进行并发编程会增加代码的复杂性。需要仔细管理和处理锁的获取和释放,以确保正确的同步和避免竞态条件。

(4)容易出错:并发编程中使用锁容易出现一些常见的问题,如死锁、活锁、饥饿等。需要仔细设计和实施锁机制,避免这些问题的发生。

(5)锁粒度问题:锁的粒度选择也是一个关键问题。锁的粒度过大会导致并发性能降低,锁的粒度过小可能会导致频繁的锁竞争和上下文切换,同样也会降低性能。

综上所述,虽然使用锁可以解决并发编程中的共享状态访问问题,但需要权衡锁带来的性能损失、复杂性增加和潜在的死锁风险。在设计并发程序时,需要综合考虑其他并发编程技术和数据结构,以选择最合适的解决方案。

你可能感兴趣的:(python,python)