【问题标题】:Use Multiprocessing to update values in a list of class instances使用多处理更新类实例列表中的值
【发布时间】:2020-11-23 20:21:10
【问题描述】:

我看到了一些建议,我什至不应该使用多处理来执行此操作,或者这不是使用多处理(内存共享)的方式,但我还是要问一下,看看我是否能更好地理解它。

我有一个 Person 类,我想使用 update_age 方法更新那个人的年龄。假设我有数百万个 Person 对象(而不是这里显示的 4 个),我想使用 Multiprocessing 来更新我拥有的所有 Person 实例的年龄。

class Person():
    def __init__(self, age):
        self.age = age

    def update_age(self):
        self.age += 10

people = [Person(1), Person(2), Person(3), Person(4)]

p = Pool(4)

results = [p.apply_async(person.update_age, ()) for person in people]

print([res.get() for res in results])

这会给我[None, None, None, None],因为方法 Person.update_age 没有返回任何东西。如果我将课程更改为:

class Person():
    def __init__(self, age):
        self.age = age

    def update_age(self):
        return self.age + 10

我会得到正确的回复[11, 12, 13, 14]。但这需要我重构该类中的所有方法。如果我在不考虑可伸缩性的情况下开发 Person 类,那就需要做很多工作。 有什么方法可以让我保持课程的原始结构,并且仍然能够使用多处理将工作负载分散到我的所有 CPU 上?

编辑:所以如果我在父进程中有这样的方法。

def update_all():
  for person in people:
    person.update_age()

使用所有 CPU 更新所有“人”的正确方法是什么?

【问题讨论】:

  • 您可以尝试使用多线程 map() 而不是多处理。见concurrent.futures.Executor#map
  • 需要明确的是,这对multiprocessing 没有意义。您派生或派生工作进程的实例,它们的对象不再连接到父进程的对象。因此,就地更新Person 实例只会更改工作端的副本,而不是父端。使用来自multiprocessing 的共享类型让你的整个班级意识到多处理可以使这项工作有效,但这是一个糟糕的主意。最好的解决方案是纯功能解决方案;传入实例,接收新实例以替换它们,不要改变现有实例。
  • @ShadowRanger 我喜欢你的建议,这正是我所期待的。但让我改写一下。你是说如果我想使用多个CPU核心,我应该用params和return编写所有函数吗?这是否意味着我根本不能在那些地方使用 OOP?我无法避免这里的重构?
  • @yangliu2:基本上,是的。就像我说的那样,旨在通过共享内存(或代理)跨进程共享数据的类在理论上可以工作,但它既丑陋又毫无意义(你会在 IPC 上花费这么多时间,你不妨在父进程中运行它过程)。
  • @ShadowRanger 感谢您的回复。我最后更新了我原来的问题,请让我知道在父进程中正确的方法是什么。谢谢!

标签: python oop multiprocessing


【解决方案1】:

您可以将自定义函数传递给多处理池,而无需修改 Person 类。但是,在当前情况下,您实际上并不需要多处理,因为您的 update_age 是非常简单(计算)的函数。

不过,如果您的 update_age 函数需要 1 毫秒才能完成,那么使用多处理是合理的。

这是一个例子,我在更新年龄中添加了一个小延迟,并引入了外部函数来对每个人执行:

import time
import copy
import random
import multiprocessing as mp
from multiprocessing.pool import Pool


class Person():
    def __init__(self, age):
        self.age = age

    def update_age(self):
        self.age += 10
        time.sleep(0.001)


def get_updated_age(person: Person) -> int:
    person.update_age()
    return person.age


if __name__ == "__main__":
    people = [Person(random.randint(0, 60)) for _ in range(int(1e4))]
    people_copy = copy.deepcopy(people)

    start = time.perf_counter()
    results_loop = []
    for person in people:
        person.update_age()
        results_loop.append(person.age)
    print(f'Apply with loop took {time.perf_counter() - start} seconds')
    
    p = Pool(mp.cpu_count())

    start = time.perf_counter()
    results_map = p.map(get_updated_age, people_copy)
    print(f'Apply with pool map took {time.perf_counter() - start} seconds')

    print(results_loop == results_map)

我已经创建了 10,000 个年龄随机的人,并按顺序(在循环中)应用了 update_age,并使用了多处理池。输出如下:

Apply with loop took 13.693018896 seconds
Apply with pool map took 3.141904311000001 seconds
True

因此,您可以看到带有外部函数的多处理可用于更有效地执行 cpu-bound 操作。

否则,如果操作不是 cpu 密集型的(如您原来的 +10),由于进程创建和池操作成本,顺序代码将优于多处理。如果我删除time.sleep(0.001),上述代码的输出将是

Apply with loop took 0.003054251000000008 seconds
Apply with pool map took 0.01680525399999999 seconds
True

因此,在这种情况下,多处理没有用。

【讨论】:

  • cProfile 告诉我,运行 4000 个我的 update_age 等效方法大约需要 0.3 秒。我想在模拟中进行数百次甚至数千次的这些更新。这就是为什么我试图将该功能扩展到多个核心。我最初担心在从多处理中获得更新的值后,我必须分配 Person 对象的所有统计信息。拥有数百万个 Person 对象,可能不值得进行多处理。现在想想,我应该去寻找蒙特卡罗模拟包,而不是自己做。
  • @yangliu2 “在我从多处理中获得更新的值后,我必须分配 Person 对象的所有统计信息”是什么意思?在pool.map 函数完成后,每个人对象已经包含一个更新的age 值。如果不想返回年龄,可以省略return person.age
  • 我误解了,我想我希望 Person 对象返回。我稍微修改了一下,我想我明白了。
  • @yangliu2 那么,这个想法对你有用吗?另外,您为什么要返回 Person 对象?您已经有一个在内部修改的 Person 对象列表。在我的示例中,people_copy 将包含针对 pool.map 方式修改的 Person 对象。
  • 所以在父进程中,我有一个更新整个模拟环境的方法,我需要跟踪Person对象。你的意思是当我执行results_map = p.map(get_updated_age, people_copy) 时,我会更新people_copy 中的所有Person 对象?我假设不是,所以我仍然想使用 results_map 作为列表来更新我的Person 列表。
猜你喜欢
  • 1970-01-01
  • 2020-02-20
  • 1970-01-01
  • 2014-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-17
  • 1970-01-01
相关资源
最近更新 更多