Python进阶系列 - 16讲 多进程

130 阅读6分钟

在这篇文章中我们讨论如何在Python中使用multiprocessing模块。

  • 如何创建并启动多个进程
  • 如何等待进程结束
  • 如何在进程间共享数据
  • 如何使用锁来防止竞争条件
  • 如何使用Queue进行流程安全的数据/任务处理。
  • 如何使用Pool管理多个工作进程

创建和运行进程

你可以使用multiprocessing.Process()来创建一个进程。它有两个重要的参数:

  • target: 一个可调用对象(函数),这个进程将在进程启动时被调用。
  • args: 函数参数。元组。

实例:

from multiprocessing import Process
import os


def square_numbers():
    for i in range(1000):
        result = i * i

if __name__ == "__main__":
    processes = []
    
    num_processes = os.cpu_count()
    print(num_processes) # 计算机中的CPU数量
    # 构造多个进程
    for _ in range(num_processes):
        process = Process(target=square_numbers)
        processes.append(process)
    # 执行进程、等待进程结束
    for process in processes:
        process.start()
        process.join()

在进程间共享数据

既然进程不在同一个内存空间,那么它们就没有访问同一个(公开)数据的权限。 因此,它们需要特殊的共享内存对象来共享数据。

数据可以通过ValueArray来存储在共享内存中:

  • Value(type, value): 创建一个ctypes对象的类型是type。访问值使用.target
  • Array(type, value): 创建一个ctypes数组,其元素的类型是type。访问值使用[]

任务:创建两个进程,每个进程都应该访问共享变量,并修改它(在这种情况下,只增加它重复100次)。 创建两个进程,每个进程都应该访问共享变量,并修改它(在这种情况下,只增加它重复100次)。

from multiprocessing import Process, Value, Array
import time

# 增加100次
def add_100(number):
    for _ in range(100):
        time.sleep(0.01)
        number.value += 1

# number中每个位置增加100次
def add_100_array(numbers):
    for _ in range(100):
        time.sleep(0.01)
        for i in range(len(numbers)):
            numbers[i] += 1


if __name__ == "__main__":
    shared_number = Value("i", 0)
    print("Value at beginning:", shared_number.value)
    shared_array = Array("d", [0.0, 100.0, 200.0])
    print("Array at beginning:", shared_array[:])
    process1 = Process(target=add_100, args=(shared_number,))
    process2 = Process(target=add_100, args=(shared_number,))
    process3 = Process(target=add_100_array, args=(shared_array,))
    process4 = Process(target=add_100_array, args=(shared_array,))
    process1.start()
    process2.start()
    process3.start()
    process4.start()
    process1.join()
    process2.join()
    process3.join()
    process4.join()
    print("Value at end:", shared_number.value)
    print("Array at end:", shared_array[:])
    print("end main")

结果:

Value at beginning: 0
Array at beginning: [0.0, 100.0, 200.0]
Value at end: 200
Array at end: [195.0, 295.0, 395.0]
end main

为什么有锁“Lock”

注意,在上面的例子中,两个进程应该通过100次增加共享值。 这个结果是200个操作。但是为什么结果不是200?

竞争条件

这里发生了竞争条件:当两个或多个进程或进程可以访问共享数据并且他们尝试同时更改它时,就会出现竞争条件。

在我们的示例中,两个进程必须读取共享值,将其加1,然后将其写回共享变量。 如果这种情况同时发生,则两个进程读取相同的值,将其增加并写回。 因此,两个进程将相同的增加值写回共享对象,并且该值没有增加 2。

有关竞争条件的详细说明,请参考xxx

通过锁来解决竞争条件

锁(也称为互斥锁)是一种同步机制,用于在有许多执行进程/进程的环境中强制限制对资源的访问。 Lock 有两种状态:lockedunlocked。 如果状态被锁定,则不允许其他并发进程/进程进入此代码段,直到状态再次解锁。 两个功能很重要:

  • lock.acquire() : 这将锁定状态并阻塞
  • lock.release() : 这将再次解锁状态。

重要提示:您应该始终在获得锁后再次释放它!

在我们的示例中,读取和增加共享变量的关键代码部分现在被锁定。 这可以防止第二个进程同时修改共享对象。 代码没有太大变化,所有新更改都在下面的代码中注释。

代码:

from multiprocessing import Lock
from multiprocessing import Process, Value, Array
import time
def add_100(number, lock):
    for _ in range(100):
        time.sleep(0.01)
        lock.acquire() # 上锁
        number.value += 1
        lock.release() # 解锁
def add_100_array(numbers, lock):
    for _ in range(100):
        time.sleep(0.01)
        for i in range(len(numbers)):
            lock.acquire() # 上锁
            numbers[i] += 1
            lock.release() # 解锁
if __name__ == "__main__":
    lock = Lock() # 创建锁
    shared_number = Value('i', 0) 
    print('Value at beginning:', shared_number.value)
    shared_array = Array('d', [0.0, 100.0, 200.0])
    print('Array at beginning:', shared_array[:])
    # 锁定共享资源
    process1 = Process(target=add_100, args=(shared_number, lock))
    process2 = Process(target=add_100, args=(shared_number, lock))
    process3 = Process(target=add_100_array, args=(shared_array, lock))
    process4 = Process(target=add_100_array, args=(shared_array, lock))
    process1.start()
    process2.start()
    process3.start()
    process4.start()
    process1.join()
    process2.join()
    process3.join()
    process4.join()
    print('Value at end:', shared_number.value)
    print('Array at end:', shared_array[:])
    print('end main')

结果:

Value at beginning: 0
Array at beginning: [0.0, 100.0, 200.0]
Value at end: 200
Array at end: [200.0, 300.0, 400.0]
end main

通过上下文管理器来使用锁

lock.acquire() 之后,你永远不要忘记调用 lock.release() 来解锁代码。 还可以使用锁作为上下文管理器,它将安全地锁定和解锁您的代码。

推荐这样使用锁:

def add_100(number, lock):
    for _ in range(100):
        time.sleep(0.01)
        with lock: #上下文方式
            number.value += 1

通过队列来使用进程

数据也可以通过队列在进程之间共享。 队列可用于多进程和多处理环境中的进程安全的进行数据交换和数据处理, 这意味着我们可以避免使用任何同步原语,如锁。

队列

队列是遵循先进先出 (FIFO) 原则的线性数据结构。 一个很好的例子是排队等候的客户队列,首先为先到的客户提供服务。

from multiprocessing import Queue

q = Queue()  # 创建一个队列
# 增加一个元素
q.put(1)  # 1
q.put(2)  # 2 1
q.put(3)  # 3 2 1 
# 输出存储形式:
# 后 --> 3 2 1 --> 前
# 取出元素会是最前面的
first = q.get()  # --> 1
print(first)
# 取出后,队列中只剩下 3 2 

在多进程中使用队列

带有队列的操作是进程安全的。多处理队列实现了队列的所有方法。

除了 task_done()join() 之外的队列。

重要的方法是:

  • q.get() : 删除并返回第一项。 默认情况下,它会阻塞,直到该项目可用。
  • q.put(item) : 将元素放在队列的末尾。 默认情况下,它会阻塞,直到有空闲插槽可用。
  • q.empty() : 如果队列为空,则返回 True。
  • q.close() : 表示当前进程不会再将数据放入此队列。

示例代码:

from multiprocessing import Process, Queue


def square(numbers, queue):
    for i in numbers:
        queue.put(i * i)


def make_negative(numbers, queue):
    for i in numbers:
        queue.put(i * -1)


if __name__ == "__main__":
    numbers = range(1, 6)
    q = Queue()
    p1 = Process(target=square, args=(numbers, q))
    p2 = Process(target=make_negative, args=(numbers, q))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
    while not q.empty():
        print(q.get())
    print("end main")

结果:

1
4
9
16
25
-1
-2
-3
-4
-5
end main

通过进程池来使用进程

进程池对象控制可以向其提交作业的工作进程池它支持具有超时和回调的异步结果,并具有并行映射实现。

它可以自动管理可用的处理器并将数据拆分成更小的块,然后可以由不同的进程并行处理。看 docs.python.org/3.7/library…

对于所有可能的方法。重要的方法是:

  • map(func, iterable[, chunksize]) :此方法将可迭代对象分割成若干块,作为单独的任务提交给进程池。这些块的(近似)大小可以通过将 chunksize 设置为正整数来指定。它阻塞,直到结果准备好。
  • close() : 防止任何更多的任务被提交到池中。完成所有任务后,工作进程将退出。
  • join():等待工作进程退出。在使用 join() 之前必须调用 close()terminate()
  • apply(func, args):使用参数 args 调用 func。它阻塞,直到结果准备好。 func 仅在池的一个工人中执行。 注意:还有异步变体map_async()apply_async() 那不会阻塞。当结果准备好时,他们可以执行回调。

代码:

from multiprocessing import Pool


def cube(number):
    return number * number * number


if __name__ == "__main__":
    numbers = range(10)
    p = Pool()
    result = p.map(cube, numbers)
    p.close()
    p.join()
    print(result)

结果:

[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]