多处理中的共享内存


74

我有三个大名单。前一个包含位数组(模块位数组0.8.0),另外两个包含整数数组。

l1=[bitarray 1, bitarray 2, ... ,bitarray n]
l2=[array 1, array 2, ... , array n]
l3=[array 1, array 2, ... , array n]

这些数据结构占用相当多的RAM(总计约16GB)。

如果我使用以下方法启动12个子流程:

multiprocessing.Process(target=someFunction, args=(l1,l2,l3))

这是否意味着将为每个子流程复制l1,l2和l3,或者子流程将共享这些列表?或者更直接地说,我将使用16GB还是192GB的RAM?

someFunction将从这些列表中读取一些值,然后根据读取的值执行一些计算。结果将返回到父进程。列表l1,l2和l3不会被someFunction修改。

因此,我假设子流程不需要并且不会复制这些庞大的列表,而只会与父级共享它们。意味着由于linux下的写时复制方法,该程序将占用16GB的RAM(无论我启动了多少个子进程)?我是对的还是我遗漏了一些会导致列表被复制的东西?

编辑:在阅读了有关该主题的更多内容后,我仍然感到困惑。一方面,Linux使用写时复制,这意味着没有数据被复制。另一方面,访问对象将更改其引用计数(我仍然不确定为什么以及这意味着什么)。即使这样,会复制整个对象吗?

例如,如果我定义someFunction如下:

def someFunction(list1, list2, list3):
    i=random.randint(0,99999)
    print list1[i], list2[i], list3[i]

使用此功能是否意味着将为每个子流程完全复制l1,l2和l3?

有办法检查吗?

EDIT2在多了一点并监视子进程运行时系统的总内存使用情况之后,似乎确实为每个子进程复制了整个对象。这似乎是因为引用计数。

在我的程序中,实际上不需要l1,l2和l3的引用计数。这是因为l1,l2和l3将保留在内存中(不变),直到父进程退出。在此之前,无需释放这些列表使用的内存。实际上,我可以肯定的是,在程序退出之前,引用计数将保持高于0(对于这些列表和这些列表中的每个对象)。

所以现在的问题变成了,我如何确保对象不会复制到每个子流程?我是否可以禁用这些列表以及这些列表中每个对象的引用计数?

EDIT3只是一个附加说明。子进程并不需要修改l1l2l3或在这些列表中的任何对象。子流程仅需要能够引用其中一些对象,而无需为每个子流程复制内存。


stackoverflow.com/questions/10721915/…类似的问题和您的答案。
肖恩

Reaad尝试通过它,仍然不确定答案。是否会复制整个对象?只是物体的一部分?仅包含引用计数的页面?我该如何检查?
FableBlaze

由于写时复制,我认为您不必做任何特别的事情。为什么不尝试一下呢?
NPE

3
尝试了一下,列表被复制了。这似乎是因为,如果我在子过程中执行l1_0 = l1 [0],则这会增加l1的引用计数器。因此,尽管我没有更改数据,但我更改了对象,这导致内存被复制。
FableBlaze 2013年

2
@ anti666非常感谢您的帖子/问题。我认为我在引用计数等方面也遇到了一些相同的问题。您是否尝试过一个Numpy数组,以至少减少可能要计算其引用的对象?另外,由于您没有提及测量方法,因此请务必使用smem的PSS统计信息;仅仅查看RSS并不会显示任何有用的信息,因为它将共享内存加倍计数。
gatoatigrado 2014年

Answers:


56

一般来说,共有两种数据共享方式:

  • 多线程
  • 共享内存

Python的多线程不适用于受CPU约束的任务(由于GIL),因此在这种情况下,通常的解决方案是继续multiprocessing。但是,使用此解决方案,您需要使用multiprocessing.Value和显式共享数据multiprocessing.Array

注意,由于所有同步问题,通常在进程之间共享数据可能不是最佳选择。通常,涉及参与者交换消息的方法是更好的选择。另请参阅Python文档

如上所述,在进行并行编程时,通常最好尽可能避免使用共享状态。使用多个进程时尤其如此。

但是,如果确实需要使用某些共享数据,则多处理提供了两种方法。

在您的情况下,您需要包装l1l2l3以某种可以理解的方式multiprocessing(例如通过使用multiprocessing.Array)进行包装,然后将它们作为参数传递。
还要注意,正如您所说的那样,您不需要写访问权,那么lock=False在创建对象时应该通过,否则所有访问权仍将被序列化。


我可以使用multiprocessing.Array包装任意对象列表bitarray()吗?
FableBlaze 2013年

1
或者,如果位数组支持协议缓冲区,则可以将其共享为字节数组,然后在生成的进程中将其转换回位数组。
2013年

1
决定把l2l3成“multiprocessing.Array”对象的元组。希望这些对象(数据的最大部分)不会为每个子流程完全复制。这将在某种程度上缓解该问题。最终的解决方案是用C重写程序,因为它将更快并且没有此问题。
FableBlaze

2
使用共享内存,即使在Python中,您也根本不会遇到这个问题。

3
multiprocessing.Value和multiprocessing.Array强制您使用原始C数据类型。他们确实确保共享内存,但这并不只是使用Linux的CoW行为那么简单,问题帖子就在此询问。我对质问者的直觉是直觉的,即引用计数令人震惊是正确的。
gatoatigrado 2014年

38

因为这在Google上仍然是很高的结果,并且没有人提及它,所以我想我会提到python版本3.8.0中引入的'true'共享内存的新可能性:https://docs.python .org / 3 / library / multiprocessing.shared_memory.html

我在这里提供了一个小巧的示例(在linux上测试过),其中使用了numpy数组,这可能是一个非常常见的用例:

# one dimension of the 2d array which is shared
dim = 5000

import numpy as np
from multiprocessing import shared_memory, Process, Lock
from multiprocessing import cpu_count, current_process
import time

lock = Lock()

def add_one(shr_name):

    existing_shm = shared_memory.SharedMemory(name=shr_name)
    np_array = np.ndarray((dim, dim,), dtype=np.int64, buffer=existing_shm.buf)
    lock.acquire()
    np_array[:] = np_array[0] + 1
    lock.release()
    time.sleep(10) # pause, to see the memory usage in top
    print('added one')
    existing_shm.close()

def create_shared_block():

    a = np.ones(shape=(dim, dim), dtype=np.int64)  # Start with an existing NumPy array

    shm = shared_memory.SharedMemory(create=True, size=a.nbytes)
    # # Now create a NumPy array backed by shared memory
    np_array = np.ndarray(a.shape, dtype=np.int64, buffer=shm.buf)
    np_array[:] = a[:]  # Copy the original data into shared memory
    return shm, np_array

if current_process().name == "MainProcess":
    print("creating shared block")
    shr, np_array = create_shared_block()

    processes = []
    for i in range(cpu_count()):
        _process = Process(target=add_one, args=(shr.name,))
        processes.append(_process)
        _process.start()

    for _process in processes:
        _process.join()

    print("Final array")
    print(np_array[:10])
    print(np_array[10:])

    shr.close()
    shr.unlink()

请注意,由于是64位整数,因此该代码可能需要大约1gb的内存才能运行,因此请确保您不会冻结使用它的系统。^ _ ^


11

如果您想使用写时复制功能并且您的数据是静态的(在子进程中不变)-您应该使python不要与您数据所在的内存块混淆。您可以通过使用C或C ++结构(例如,stl)作为容器来轻松地做到这一点,并提供自己的python包装器,该包装器将在创建python级对象(如果有的话)时使用指向数据内存的指针(或可能复制数据mem)。 。几乎可以通过python简单性和cython语法轻松地完成所有这些工作。

#伪cython
cdef类FooContainer:
   cdef char *数据
   def __cinit __(self,char * foo_value):
       self.data = malloc(1024,sizeof(char))
       memcpy(self.data,foo_value,min(1024,len(foo_value))))
   
   def get():
       返回self.data

#python部分
从foo导入FooContainer

f = FooContainer(“ hello world”)
pid = fork()
如果不是pid:
   f.get()#此调用将相同的内存页读取到
           #父进程写了1024个字符的self.data
           #和cython将自动创建一个新的python字符串
           #对象,然后返回到调用者

上面的伪代码写得不好。不要使用它。在您的情况下,应使用C或C ++容器代替self.data。


3

您可以使用memcached或redis并将它们分别设置为键值对{'l1'...


我认为redis阻止了。因此,如果需要多个读取器访问共享结构,则mp.Array / mp.Value可能是一个更好的解决方案。这一切都取决于应用程序
Cryptoharf84
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.