개발/Python

[파이썬] 공유메모리의 멀티 프로세싱

MinorMan 2022. 10. 9. 04:01
반응형

<질문>

세 개의 큰 목록이 있습니다. 첫 번째는 bitarrays(모듈 bitarray 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에서 copy-on-write 접근 방식으로 인해 프로그램이 시작하는 하위 프로세스 수에 관계없이 16GB의 RAM이 필요하다는 것을 의미합니까? 내가 맞습니까 아니면 목록이 복사되도록 할 수 있는 것을 놓치고 있습니까?

편집: 나는 주제에 대해 조금 더 읽은 후에도 여전히 혼란스러워합니다. 한편으로 Linux는 copy-on-write를 사용하므로 데이터가 복사되지 않습니다. 반면에 개체에 액세스하면 참조 횟수가 변경됩니다(나는 여전히 이유와 의미가 확실하지 않습니다). 그럼에도 불구하고 전체 개체가 복사됩니까?

예를 들어 내가 someFunction을 다음과 같이 정의한다면:

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

이 기능을 사용하면 각 하위 프로세스에 대해 l1, l2 및 l3이 완전히 복사됩니까?

이를 확인할 수 있는 방법이 있습니까?

편집2 조금 더 읽고 하위 프로세스가 실행되는 동안 시스템의 총 메모리 사용량을 모니터링하면 실제로 각 하위 프로세스에 대해 전체 개체가 복사되는 것 같습니다. 그리고 레퍼런스 카운팅 때문인 것 같습니다.

l1, l2 및 l3에 대한 참조 카운팅은 실제로 내 프로그램에서 필요하지 않습니다. 이는 l1, l2 및 l3이 부모 프로세스가 종료될 때까지 메모리에 변경되지 않고 유지되기 때문입니다. 그때까지는 이 목록에서 사용하는 메모리를 해제할 필요가 없습니다. 사실 나는 프로그램이 종료될 때까지 참조 횟수가 0(이 목록과 이 목록의 모든 개체에 대해) 이상으로 유지된다는 것을 알고 있습니다.

이제 질문은 개체가 각 하위 프로세스에 복사되지 않도록 하려면 어떻게 해야 합니까? 이 목록과 이 목록의 각 개체에 대한 참조 계산을 비활성화할 수 있습니까?

편집3 추가 참고 사항입니다. 하위 프로세스는 수정할 필요가 없습니다.l1,l2 그리고l3 또는 이 목록에 있는 모든 개체. 하위 프로세스는 각 하위 프로세스에 대해 메모리를 복사하지 않고 이러한 개체 중 일부만 참조할 수 있으면 됩니다.


<답변1>

이것은 여전히 google에서 매우 높은 결과이고 아직 아무도 언급하지 않았기 때문에 python 버전 3.8.0에 도입된 '진정한' 공유 메모리의 새로운 가능성을 언급할 것이라고 생각했습니다.https://docs.python.org/3/library/multiprocessing.shared_memory.html

여기에 numpy 배열이 사용되는 작은 인위적인 예제(Linux에서 테스트됨)가 포함되어 있습니다. 이는 매우 일반적인 사용 사례일 수 있습니다.

# 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의 램이 필요할 수 있으므로 이를 사용하여 시스템을 정지시키지 않도록 하십시오. ^_^


<답변2>

일반적으로 동일한 데이터를 공유하는 두 가지 방법이 있습니다.

  • 멀티스레딩
  • 공유 메모리

Python의 멀티스레딩은 CPU 바운드 작업(GIL 때문에)에 적합하지 않으므로 이 경우 일반적인 솔루션은 계속 진행하는 것입니다.multiprocessing. 그러나 이 솔루션을 사용하면 명시적으로 데이터를 공유해야 합니다.multiprocessing.Value 그리고multiprocessing.Array.

일반적으로 프로세스 간에 데이터를 공유하는 것은 모든 동기화 문제로 인해 최선의 선택이 아닐 수 있습니다. 액터가 메시지를 교환하는 방식이 일반적으로 더 나은 선택으로 간주됩니다. 또한보십시오Python documentation:

위에서 언급했듯이 동시 프로그래밍을 할 때 일반적으로 가능한 한 공유 상태를 사용하지 않는 것이 가장 좋습니다. 이것은 여러 프로세스를 사용할 때 특히 그렇습니다. 그러나 실제로 일부 공유 데이터를 사용해야 하는 경우 멀티프로세싱은 몇 가지 방법을 제공합니다.

귀하의 경우 포장해야합니다l1,l2 그리고l3 어떻게 보면 이해할 수 있는multiprocessing (예:multiprocessing.Array), 매개변수로 전달합니다.
또한 쓰기 권한이 필요하지 않다고 말했듯이 다음을 전달해야 합니다.lock=False 개체를 만드는 동안 그렇지 않으면 모든 액세스가 계속 직렬화됩니다.


<답변3>

Python3.8 사용에 관심이 있는 분들을 위해shared_memory 모듈, 여전히bug 수정되지 않았으며 지금까지 Python3.8/3.9/3.10에 영향을 미치고 있습니다(2021-01-15). 이 버그는 posix 시스템에 영향을 미치며 다른 프로세스가 여전히 유효한 액세스 권한을 가져야 할 때 리소스 추적기가 공유 메모리 세그먼트를 파괴하는 것에 관한 것입니다. 따라서 코드에서 사용하는 경우 주의하십시오.


<답변4>

기록 중 복사 기능을 사용하고 데이터가 정적(자식 프로세스에서 변경되지 않음)인 경우 - 데이터가 있는 메모리 블록을 python이 엉망으로 만들지 않도록 해야 합니다. C 또는 C++ 구조(예: stl)를 컨테이너로 사용하고 파이썬 수준 객체가 생성될 때 데이터 메모리에 대한 포인터를 사용하는(또는 데이터 메모리를 복사할 수 있는) 자체 파이썬 래퍼를 제공하여 이를 쉽게 수행할 수 있습니다. . 이 모든 것은 거의 python 단순성과 구문으로 매우 쉽게 수행할 수 있습니다.cython.


# pseudo cython
cdef class FooContainer:
   cdef char * data
   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):
       return self.data


# python part
from foo import FooContainer

f = FooContainer("hello world")
pid = fork()
if not pid:
   f.get() # this call will read same memory page to where
           # parent process wrote 1024 chars of self.data
           # and cython will automatically create a new python string
           # object from it and return to caller

위의 의사 코드는 잘못 작성되었습니다. 그것을 사용하지 마십시오. self.data 대신 귀하의 경우 C 또는 C++ 컨테이너여야 합니다.


<답변5>

memcached 또는 redis를 사용하고 각각을 키 값 쌍으로 설정할 수 있습니다. {'l1'...

반응형